create-panal-agent 0.11.0 → 0.13.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 -0
- package/dist/index.js +4 -0
- package/package.json +6 -4
- package/template/_package.json +2 -1
- package/template/src/adjuntos.ts +359 -0
- package/template/src/agent.ts +67 -41
- package/template/src/memoria.ts +136 -0
- package/template/src/reintento.ts +68 -0
- package/template/src/salida.ts +391 -0
- package/template/src/server.ts +16 -0
- package/template/src/zip.ts +245 -0
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. 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.",
|
|
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
|
+
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.",
|
|
79
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).",
|
|
80
81
|
},
|
|
81
82
|
readme: `# {name}
|
|
@@ -171,6 +172,7 @@ El idioma sale de --lang, luego de PANAL_LANG, y si no del locale del sistema.`,
|
|
|
171
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`.',
|
|
172
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
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
|
+
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.",
|
|
174
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).",
|
|
175
177
|
},
|
|
176
178
|
readme: `# {name}
|
|
@@ -267,6 +269,7 @@ const zh = {
|
|
|
267
269
|
x402: '按次收费(可选)。留空则你的代理只接受托管订单。\n价格按每次请求计算,使用支持 EIP-2612 的代币——不能是 MON,该方案依赖 `permit`。',
|
|
268
270
|
subcontrata: "转包(可选,默认关闭)。你的代理可以为自己做不了的事付钱给另一个代理(见 agent.ts 的 ctx.consultar)。这里没有数字就永远不会转包。\n它使用 x402 的币种,不是按任务收入的比例:任务用 MON 结算,提问用 $PANAL,凭感觉换算等于凭空编造预算。 把它设得低于你每次提问的收费(X402_PRICE):等于或高于该值时,每一笔你转包出去的活都赚不到钱,还要自付 gas——而这恰恰惩罚了你希望它做的事,因为它越善于识别自己不懂的部分,就越经常白干。取你价格的三分之一是个合理的起点。",
|
|
269
271
|
vigilante: "守望者。每 60 秒检查一次你未完成的任务,并挽回卡住的部分:没有上链的交付、中途死掉的工作,或已付款却从未送达的委托。\nVIGILANTE=off 可关闭。PUBLIC_URL 是你的 https 端点,用于丢失委托的提醒。",
|
|
272
|
+
memoria: "对话记忆(仅限 x402)。没有它,每次付费调用都是独立的:客户提问,你回答,下一次你不知道之前在谈什么——那是带账单的搜索框,不是聊天。\nMEMORIA_TURNOS=0 可关闭。两个上限的存在是因为历史会进入提示词,而提示词由你付费,客户只付固定的每条消息价格。",
|
|
270
273
|
seguridad: "加固。LIMITE_POR_MINUTO 限制每个 IP 的请求数(0 表示关闭);每个未认证请求都要消耗一次 RPC 调用,没有上限时一个 curl 循环就能让你的代理无法交付。\nTRAS_PROXY=1 前面有反向代理时使用(Caddy、nginx)——这是常态,因为你的端点必须是 https。不设置时所有请求都带着代理的 IP,上面的上限就不再是每个客户端的,而变成由索引器、浏览器和你的客户共用的全局上限;一旦填满,你的代理会对所有人返回 429,而无法发送任务书的客户只能带着已锁定的付款等到期限。没有代理时请保持关闭:无代理却信任该请求头,任何人都能为每个请求伪造 IP。 AUTH_ESTRICTA=1 拒绝没有有效期的下载签名(旧格式)。",
|
|
271
274
|
},
|
|
272
275
|
readme: `# {name}
|
|
@@ -358,6 +361,7 @@ const hi = {
|
|
|
358
361
|
x402: 'प्रति कॉल शुल्क (वैकल्पिक)। खाली छोड़ें तो आपका एजेंट केवल एस्क्रो वाले काम लेगा।\nकीमत प्रति अनुरोध है, EIP-2612 टोकन में — MON नहीं चल सकता, इस योजना को `permit` चाहिए।',
|
|
359
362
|
subcontrata: "सबकॉन्ट्रैक्टिंग (वैकल्पिक, डिफ़ॉल्ट रूप से बंद)। आपका एजेंट जो खुद नहीं कर सकता, उसके लिए दूसरे को भुगतान कर सकता है (agent.ts में ctx.consultar देखें)। यहाँ संख्या के बिना वह कभी नहीं सौंपता।\nयह x402 की मुद्रा में है, प्रति कार्य आय का हिस्सा नहीं: कार्य MON में और प्रश्न $PANAL में चुकाया जाता है। इसे अपने प्रति-प्रश्न शुल्क (X402_PRICE) से कम रखें: बराबर या अधिक होने पर, जिस भी काम में आप दूसरे को सौंपेंगे उसमें कुछ नहीं बचेगा और गैस भी आपकी जेब से जाएगी — और यह ठीक उसी व्यवहार को दंडित करता है जो आप चाहते हैं, क्योंकि आपका एजेंट जितना बेहतर पहचानेगा कि उसे क्या नहीं आता, उतनी ही बार वह मुफ़्त काम करेगा। अपने दाम का एक तिहाई एक समझदार शुरुआत है।",
|
|
360
363
|
vigilante: "प्रहरी। हर 60 सेकंड में आपके खुले कार्यों की जाँच करता है और जो अटक गया उसे वापस लाता है: वह डिलीवरी जो चेन पर दर्ज नहीं हुई, अधूरा रह गया काम, या भुगतान किया गया आदेश जो कभी नहीं पहुँचा।\nVIGILANTE=off इसे बंद करता है। PUBLIC_URL आपका https एंडपॉइंट है।",
|
|
364
|
+
memoria: "बातचीत की स्मृति (केवल x402)। इसके बिना हर सशुल्क कॉल स्वतंत्र होती है: ग्राहक पूछता है, आप उत्तर देते हैं, और अगली बार आपको पता नहीं होता कि बात किस बारे में थी — वह चैट नहीं, बिल वाला सर्च बॉक्स है।\nMEMORIA_TURNOS=0 इसे बंद कर देता है। दोनों सीमाएँ इसलिए हैं कि इतिहास प्रॉम्प्ट में जाता है, और प्रॉम्प्ट का भुगतान आप करते हैं जबकि ग्राहक प्रति संदेश निश्चित मूल्य देता है।",
|
|
361
365
|
seguridad: "सुरक्षा सख्ती। LIMITE_POR_MINUTO प्रति IP अनुरोध सीमित करता है (0 से बंद); हर बिना प्रमाणित अनुरोध एक RPC कॉल खर्च करता है, इसलिए बिना सीमा के एक curl लूप आपके एजेंट को डिलीवर करने से रोक सकता है।\nTRAS_PROXY=1 अगर आगे रिवर्स प्रॉक्सी हो (Caddy, nginx) — यही सामान्य है, क्योंकि आपका एंडपॉइंट https होना चाहिए। इसके बिना हर अनुरोध प्रॉक्सी के IP के साथ आता है, इसलिए ऊपर की सीमा प्रति-क्लाइंट नहीं रहती और एक वैश्विक सीमा बन जाती है जिसे इंडेक्सर, ब्राउज़र और आपके क्लाइंट साझा करते हैं; भर जाने पर आपका एजेंट सबको 429 देता है, और जो क्लाइंट अपना ब्रीफ़ नहीं भेज पाता वह भुगतान लॉक होने के बावजूद समय-सीमा तक इंतज़ार करता है। प्रॉक्सी न हो तो बंद रखें: बिना प्रॉक्सी उस हेडर पर भरोसा करने से कोई भी हर अनुरोध में मनमाना IP भेज सकता है। AUTH_ESTRICTA=1 बिना समय-सीमा वाले डाउनलोड हस्ताक्षर अस्वीकार करता है।",
|
|
362
366
|
},
|
|
363
367
|
readme: `# {name}
|
|
@@ -453,6 +457,7 @@ const ar = {
|
|
|
453
457
|
x402: 'التحصيل لكل استدعاء (اختياري). اتركه فارغًا فيقبل وكيلك مهام الضمان فقط.\nالسعر لكل طلب، بعملة تدعم EIP-2612 — لا يصلح MON، فالمخطط يحتاج `permit`.',
|
|
454
458
|
subcontrata: "التعاقد من الباطن (اختياري، معطل افتراضيا). يمكن لوكيلك ان يدفع لوكيل اخر مقابل ما لا يجيده (انظر ctx.consultar في agent.ts). بدون رقم هنا لن يفوض ابدا.\nيكون بعملة x402، وليس نسبة مما تتقاضاه عن المهمة: المهمة تدفع بـ MON والسؤال بـ $PANAL. اجعله أقل من سعر السؤال لديك (X402_PRICE): فعند التساوي أو الزيادة، كل مهمة تُسنِد فيها جزءًا إلى غيرك لا تُبقي لك شيئًا وتدفع الغاز من جيبك — وهذا يعاقب تحديدًا ما تريده منه، لأنه كلما أحسن إدراك ما يجهله عمل مجانًا أكثر. ثلث سعرك بداية معقولة.",
|
|
455
459
|
vigilante: "الحارس. كل 60 ثانية يراجع مهامك المفتوحة ويستعيد ما تعطل: تسليم لم يثبت على السلسلة، او عمل مات في منتصفه، او طلب مدفوع لم يصل قط.\nVIGILANTE=off يوقفه. PUBLIC_URL هو نقطة الوصول https لديك.",
|
|
460
|
+
memoria: "ذاكرة المحادثة (في x402 فقط). بدونها تكون كل مكالمة مدفوعة مستقلة: يسأل العميل، تجيب، وفي المرة التالية لا تعرف عمّا كان الحديث — هذا مربع بحث بفاتورة، لا محادثة.\nالقيمة MEMORIA_TURNOS=0 تُعطّلها. الحدّان موجودان لأن السجل يدخل في التوجيه، والتوجيه تدفعه أنت بينما يدفع العميل سعرًا ثابتًا لكل رسالة.",
|
|
456
461
|
seguridad: "تقوية. LIMITE_POR_MINUTO يحد الطلبات لكل IP (0 يعطله)؛ كل طلب غير موثق يكلف استدعاء RPC، فبدون حد يكفي حلقة curl لتترك وكيلك عاجزا عن التسليم.\nTRAS_PROXY=1 إذا كان أمامه بروكسي عكسي (Caddy أو nginx) — وهو الوضع المعتاد لأن نقطة النهاية يجب أن تكون https. بدونه تصل كل الطلبات بعنوان IP الخاص بالبروكسي، فيتوقف الحد أعلاه عن كونه لكل عميل ويصبح حدًا عامًا واحدًا يتقاسمه المفهرس والمتصفحات وعملاؤك؛ وعند امتلائه يرد وكيلك بـ 429 على الجميع، ويظل العميل الذي تعذر عليه إرسال مهمته ينتظر انتهاء المهلة ودفعته محجوزة بالفعل. بلا بروكسي اتركه معطلًا: الوثوق بهذه الترويسة دون بروكسي يتيح لأي شخص اختلاق عنوان IP لكل طلب. AUTH_ESTRICTA=1 يرفض تواقيع التنزيل بلا انتهاء صلاحية.",
|
|
457
462
|
},
|
|
458
463
|
readme: `# {name}
|
|
@@ -547,6 +552,7 @@ La langue vient de --lang, puis de PANAL_LANG, puis de la locale du système.`,
|
|
|
547
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`.",
|
|
548
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.",
|
|
549
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
|
+
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.",
|
|
550
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.",
|
|
551
557
|
},
|
|
552
558
|
readme: `# {name}
|
|
@@ -645,6 +651,7 @@ O idioma vem de --lang, depois de PANAL_LANG e, por fim, do locale do sistema.`,
|
|
|
645
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`.',
|
|
646
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.",
|
|
647
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
|
+
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.",
|
|
648
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.",
|
|
649
656
|
},
|
|
650
657
|
readme: `# {name}
|
|
@@ -741,6 +748,7 @@ const ru = {
|
|
|
741
748
|
x402: 'Плата за вызов (необязательно). Оставьте пустым — агент будет брать только заказы через эскроу.\nЦена за один запрос, в токене с EIP-2612: MON не подходит, схеме нужен `permit`.',
|
|
742
749
|
subcontrata: "Субподряд (необязательно, по умолчанию выключен). Ваш агент может заплатить другому за то, чего не умеет сам (см. ctx.consultar в agent.ts). Без числа здесь он никогда не делегирует.\nУказывается в валюте x402, а НЕ долей от оплаты за задачу: задача оплачивается в MON, а вопрос в $PANAL. Поставьте НИЖЕ того, что берёте за вопрос (X402_PRICE): при равенстве или выше каждая работа, где вы делегируете, не приносит ничего, а газ вы платите сами — и это наказывает ровно то поведение, которого вы добиваетесь: чем лучше агент понимает, чего не знает, тем чаще работает даром. Треть вашей цены — разумное начало.",
|
|
743
750
|
vigilante: "Сторож. Каждые 60 с проверяет ваши открытые задачи и вытаскивает то, что застряло: доставку, которая не попала в блокчейн, работу, оборвавшуюся на середине, или оплаченный заказ, который так и не пришел.\nVIGILANTE=off выключает его. PUBLIC_URL ваш https-эндпоинт.",
|
|
751
|
+
memoria: "Память разговора (только x402). Без неё каждый оплаченный вызов независим: клиент спрашивает, вы отвечаете, а в следующий раз вы не знаете, о чём шла речь — это поисковая строка со счётом, а не чат.\nMEMORIA_TURNOS=0 отключает её. Оба предела существуют потому, что история попадает В промпт, а промпт оплачиваете вы, тогда как клиент платит фиксированную цену за сообщение.",
|
|
744
752
|
seguridad: "Усиление защиты. LIMITE_POR_MINUTO ограничивает запросы с одного IP (0 отключает); каждый неаутентифицированный запрос стоит одного вызова RPC, поэтому без лимита цикл curl лишает агента возможности сдавать работу.\nTRAS_PROXY=1 если впереди стоит обратный прокси (Caddy, nginx) — обычный случай, ведь ваш эндпоинт должен быть https. Без этого все запросы приходят с IP прокси, поэтому лимит выше перестаёт быть на клиента и становится единым глобальным, который делят индексатор, браузеры и ваши клиенты; когда он заполняется, ваш агент отвечает 429 всем, а клиент, не сумевший отправить задание, ждёт истечения срока с уже заблокированной оплатой. Без прокси оставьте выключенным: доверять этому заголовку без прокси — значит позволить подделывать IP в каждом запросе. AUTH_ESTRICTA=1 отклоняет подписи скачивания без срока действия.",
|
|
745
753
|
},
|
|
746
754
|
readme: `# {name}
|
|
@@ -838,6 +846,7 @@ const bn = {
|
|
|
838
846
|
x402: 'প্রতি কলে চার্জ (ঐচ্ছিক)। ফাঁকা রাখলে আপনার এজেন্ট কেবল এসক্রো কাজ নেবে।\nদাম প্রতি অনুরোধে, EIP-2612 টোকেনে — MON চলবে না, স্কিমটির `permit` দরকার।',
|
|
839
847
|
subcontrata: "সাবকন্ট্রাক্টিং (ঐচ্ছিক, ডিফল্টে বন্ধ)। আপনার এজেন্ট যা নিজে পারে না, তার জন্য অন্যকে অর্থ দিতে পারে (agent.ts-এ ctx.consultar দেখুন)। এখানে সংখ্যা না থাকলে সে কখনও দায়িত্ব দেয় না।\nএটি x402-এর মুদ্রায়, কাজপ্রতি আয়ের অংশ নয়: কাজের দাম MON-এ আর প্রশ্নের দাম $PANAL-এ। এটি আপনার প্রতি-প্রশ্ন মূল্যের (X402_PRICE) চেয়ে কম রাখুন: সমান বা বেশি হলে, যে কাজেই আপনি অন্যকে দেবেন তাতে কিছুই থাকবে না, গ্যাসও আপনার — আর এতে ঠিক সেই আচরণই শাস্তি পায় যা আপনি চান, কারণ এজেন্ট যত ভালোভাবে বুঝবে সে কী জানে না, তত বেশিবার সে বিনামূল্যে কাজ করবে। আপনার দামের এক-তৃতীয়াংশ একটি যুক্তিসঙ্গত শুরু।",
|
|
840
848
|
vigilante: "প্রহরী। প্রতি ৬০ সেকেন্ডে আপনার খোলা কাজগুলি দেখে এবং আটকে যাওয়া জিনিস উদ্ধার করে: চেইনে না ওঠা ডেলিভারি, মাঝপথে মরে যাওয়া কাজ, বা টাকা দেওয়া হয়েছে অথচ কখনও পৌঁছায়নি এমন আদেশ।\nVIGILANTE=off এটি বন্ধ করে। PUBLIC_URL আপনার https এন্ডপয়েন্ট।",
|
|
849
|
+
memoria: "কথোপকথনের স্মৃতি (কেবল x402)। এটি ছাড়া প্রতিটি অর্থপ্রদত্ত কল স্বতন্ত্র: ক্লায়েন্ট জিজ্ঞাসা করে, আপনি উত্তর দেন, আর পরেরবার আপনি জানেন না কী নিয়ে কথা হচ্ছিল — সেটা চ্যাট নয়, বিলসহ সার্চ বক্স।\nMEMORIA_TURNOS=0 এটি বন্ধ করে। দুটি সীমা আছে কারণ ইতিহাস প্রম্পটে ঢোকে, আর প্রম্পটের খরচ আপনি দেন যখন ক্লায়েন্ট বার্তাপ্রতি নির্দিষ্ট দাম দেয়।",
|
|
841
850
|
seguridad: "কঠোরকরণ। LIMITE_POR_MINUTO প্রতি IP অনুরোধ সীমিত করে (0 দিলে বন্ধ); প্রতিটি অননুমোদিত অনুরোধে একটি RPC কল খরচ হয়, তাই সীমা ছাড়া একটি curl লুপ আপনার এজেন্টকে ডেলিভারি করতে অক্ষম করে দেয়।\nTRAS_PROXY=1 যদি সামনে রিভার্স প্রক্সি থাকে (Caddy, nginx) — এটিই স্বাভাবিক, কারণ আপনার এন্ডপয়েন্ট https হতেই হবে। এটি ছাড়া প্রতিটি অনুরোধ প্রক্সির IP নিয়ে আসে, ফলে উপরের সীমা আর প্রতি-ক্লায়েন্ট থাকে না, বরং একটি বৈশ্বিক সীমা হয়ে যায় যা ইনডেক্সার, ব্রাউজার ও আপনার ক্লায়েন্টরা ভাগ করে; ভরে গেলে আপনার এজেন্ট সবাইকে 429 দেয়, আর যে ক্লায়েন্ট তার কাজের বিবরণ পাঠাতে পারে না সে অর্থ আটকে থাকা অবস্থায় সময়সীমা পর্যন্ত অপেক্ষা করে। প্রক্সি না থাকলে বন্ধ রাখুন: প্রক্সি ছাড়া ওই হেডারে ভরসা করলে যে কেউ প্রতি অনুরোধে IP বানিয়ে দিতে পারে। AUTH_ESTRICTA=1 মেয়াদহীন ডাউনলোড স্বাক্ষর প্রত্যাখ্যান করে।",
|
|
842
851
|
},
|
|
843
852
|
readme: `# {name}
|
|
@@ -932,6 +941,7 @@ const ur = {
|
|
|
932
941
|
x402: 'فی کال وصولی (اختیاری)۔ خالی چھوڑ دیں تو آپ کا ایجنٹ صرف ایسکرو کے کام لے گا۔\nقیمت فی درخواست ہے، EIP-2612 ٹوکن میں — MON نہیں چل سکتا، اس اسکیم کو `permit` چاہیے۔',
|
|
933
942
|
subcontrata: "سب کنٹریکٹنگ (اختیاری، بذریعہ ڈیفالٹ بند)۔ آپ کا ایجنٹ جو خود نہیں کر سکتا اس کے لیے دوسرے کو ادائیگی کر سکتا ہے (agent.ts میں ctx.consultar دیکھیں)۔ یہاں نمبر کے بغیر وہ کبھی نہیں سونپتا۔\nیہ x402 کی کرنسی میں ہے، فی ٹاسک آمدنی کا حصہ نہیں: ٹاسک MON میں اور سوال $PANAL میں ادا ہوتا ہے۔ اسے اپنی فی سوال قیمت (X402_PRICE) سے کم رکھیں: برابر یا زیادہ ہونے پر، جس کام میں بھی آپ کسی اور کو دیں گے اس میں کچھ نہیں بچے گا اور گیس بھی آپ کی جیب سے جائے گی — اور یہ بالکل اسی رویّے کو سزا دیتا ہے جو آپ چاہتے ہیں، کیونکہ آپ کا ایجنٹ جتنا بہتر پہچانے گا کہ اسے کیا نہیں آتا، اتنی ہی زیادہ بار وہ مفت کام کرے گا۔ اپنی قیمت کا ایک تہائی معقول آغاز ہے۔",
|
|
934
943
|
vigilante: "نگہبان۔ ہر 60 سیکنڈ میں آپ کے کھلے کام دیکھتا ہے اور جو اٹکا ہے اسے بچاتا ہے: وہ ڈیلیوری جو چین پر درج نہ ہوئی، وہ کام جو بیچ میں مر گیا، یا ادا شدہ آرڈر جو کبھی نہ پہنچا۔\nVIGILANTE=off اسے بند کرتا ہے۔ PUBLIC_URL آپ کا https اینڈ پوائنٹ ہے۔",
|
|
944
|
+
memoria: "گفتگو کی یادداشت (صرف x402)۔ اس کے بغیر ہر ادا شدہ کال الگ ہوتی ہے: کلائنٹ پوچھتا ہے، آپ جواب دیتے ہیں، اور اگلی بار آپ کو معلوم نہیں ہوتا کہ بات کس بارے میں تھی — یہ چیٹ نہیں، بل والا سرچ باکس ہے۔\nMEMORIA_TURNOS=0 اسے بند کر دیتا ہے۔ دونوں حدیں اس لیے ہیں کہ تاریخ پرامپٹ میں جاتی ہے، اور پرامپٹ کا خرچ آپ اٹھاتے ہیں جبکہ کلائنٹ فی پیغام مقررہ قیمت دیتا ہے۔",
|
|
935
945
|
seguridad: "سختی۔ LIMITE_POR_MINUTO فی IP درخواستیں محدود کرتا ہے (0 سے بند)؛ ہر غیر تصدیق شدہ درخواست ایک RPC کال خرچ کرتی ہے، اس لیے حد کے بغیر ایک curl لوپ آپ کے ایجنٹ کو ڈیلیور کرنے سے روک سکتا ہے۔\nTRAS_PROXY=1 اگر آگے ریورس پراکسی ہو (Caddy، nginx) — یہی معمول ہے، کیونکہ آپ کا اینڈ پوائنٹ https ہونا چاہیے۔ اس کے بغیر ہر درخواست پراکسی کے IP کے ساتھ آتی ہے، چنانچہ اوپر والی حد فی کلائنٹ نہیں رہتی بلکہ ایک عالمی حد بن جاتی ہے جسے انڈیکسر، براؤزر اور آپ کے کلائنٹ مل کر استعمال کرتے ہیں؛ بھر جانے پر آپ کا ایجنٹ سب کو 429 دیتا ہے، اور جو کلائنٹ اپنی ہدایت نہیں بھیج پاتا وہ ادائیگی مقفل ہونے کے باوجود مہلت تک انتظار کرتا ہے۔ پراکسی نہ ہو تو بند رکھیں: پراکسی کے بغیر اس ہیڈر پر بھروسہ کرنے سے کوئی بھی ہر درخواست میں IP گھڑ سکتا ہے۔ AUTH_ESTRICTA=1 بغیر میعاد والے ڈاؤن لوڈ دستخط مسترد کرتا ہے۔",
|
|
936
946
|
},
|
|
937
947
|
readme: `# {name}
|
package/dist/index.js
CHANGED
|
@@ -176,6 +176,10 @@ function writeEnvExample(dest, t) {
|
|
|
176
176
|
'VIGILANTE_SEGUNDOS=60',
|
|
177
177
|
'PUBLIC_URL=',
|
|
178
178
|
'',
|
|
179
|
+
bloque(t.env.memoria),
|
|
180
|
+
'MEMORIA_TURNOS=6',
|
|
181
|
+
'MEMORIA_CHARS=4000',
|
|
182
|
+
'',
|
|
179
183
|
bloque(t.env.seguridad),
|
|
180
184
|
'LIMITE_POR_MINUTO=60',
|
|
181
185
|
'# TRAS_PROXY=1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-panal-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
"node": ">=20"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
34
|
"tsx": "^4.19.0",
|
|
35
|
-
"
|
|
35
|
+
"typescript": "^5.6.0",
|
|
36
|
+
"unpdf": "^1.8.1",
|
|
37
|
+
"xlsx": "^0.18.5"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"viem": "^2.21.0"
|
|
@@ -40,6 +42,6 @@
|
|
|
40
42
|
"scripts": {
|
|
41
43
|
"build": "tsc -p tsconfig.json",
|
|
42
44
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
-
"test": "tsx test/i18n.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/salida.test.ts && tsx test/scaffold.test.ts"
|
|
44
46
|
}
|
|
45
47
|
}
|
package/template/_package.json
CHANGED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lo que el cliente adjuntó, convertido en algo que un modelo pueda usar.
|
|
3
|
+
*
|
|
4
|
+
* Un encargo puede traer cinco archivos de hasta 25 MB, y llegan verificados:
|
|
5
|
+
* su hash se anunció DENTRO del encargo antes de que se pagara, así que son
|
|
6
|
+
* exactamente los que el escrow cubre. Lo que hace este archivo es la otra
|
|
7
|
+
* mitad: abrirlos.
|
|
8
|
+
*
|
|
9
|
+
* Cada tipo entra por donde puede:
|
|
10
|
+
*
|
|
11
|
+
* imagen (png/jpeg/gif/webp) se le ENSEÑA al modelo, no se describe
|
|
12
|
+
* texto (código, md, csv…) tal cual
|
|
13
|
+
* PDF se le extrae el texto
|
|
14
|
+
* .docx es un ZIP con XML dentro
|
|
15
|
+
* .zip se abre y se leen los que sí se puedan
|
|
16
|
+
* cualquier otra cosa se NOMBRA, y se dice que no se pudo abrir
|
|
17
|
+
*
|
|
18
|
+
* ESE ÚLTIMO CASO NO ES UN DESCUIDO. Callar un archivo que no se pudo abrir
|
|
19
|
+
* hace que el modelo conteste como si el cliente no hubiera mandado nada, y el
|
|
20
|
+
* cliente recibe una respuesta que ignora la mitad de lo que pidió sin decir
|
|
21
|
+
* por qué. Decirlo cuesta una línea y convierte un fallo en una explicación.
|
|
22
|
+
*
|
|
23
|
+
* EL TIPO SE MIRA POR LOS BYTES, no por el nombre ni por el `content-type` que
|
|
24
|
+
* mandó el cliente: los dos los escribe él, y un `.txt` que en realidad es un
|
|
25
|
+
* PDF es un accidente normal, no un ataque.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { esZip, leerZip } from './zip.js';
|
|
29
|
+
|
|
30
|
+
export interface AdjuntoRecibido {
|
|
31
|
+
name: string;
|
|
32
|
+
mime?: string;
|
|
33
|
+
bytes: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Una imagen lista para enseñársela al modelo. */
|
|
37
|
+
export interface ImagenAdjunta {
|
|
38
|
+
mime: string;
|
|
39
|
+
bytes: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AdjuntosLeidos {
|
|
43
|
+
/** Ya etiquetado y listo para pegar al encargo. Vacío si no hay nada. */
|
|
44
|
+
texto: string;
|
|
45
|
+
/** Las que el modelo puede mirar de verdad. */
|
|
46
|
+
imagenes: ImagenAdjunta[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Tope de caracteres que aporta UN adjunto. El encargo lo paga el agente. */
|
|
50
|
+
const MAX_CHARS_POR_ADJUNTO = 8_000;
|
|
51
|
+
/** Y el de todos juntos, porque cinco archivos al tope son demasiado. */
|
|
52
|
+
const MAX_CHARS_TOTAL = 24_000;
|
|
53
|
+
/** Archivos que se miran dentro de un ZIP. */
|
|
54
|
+
const MAX_DENTRO_DEL_ZIP = 40;
|
|
55
|
+
|
|
56
|
+
const MIMES_IMAGEN: Record<string, string> = {
|
|
57
|
+
png: 'image/png',
|
|
58
|
+
jpeg: 'image/jpeg',
|
|
59
|
+
gif: 'image/gif',
|
|
60
|
+
webp: 'image/webp',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Qué es esto, mirando los primeros bytes. */
|
|
64
|
+
export function tipoDe(bytes: Uint8Array): 'png' | 'jpeg' | 'gif' | 'webp' | 'pdf' | 'zip' | 'otro' {
|
|
65
|
+
const b = bytes;
|
|
66
|
+
if (b.length < 4) return 'otro';
|
|
67
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'png';
|
|
68
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpeg';
|
|
69
|
+
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return 'gif';
|
|
70
|
+
if (b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[8] === 0x57 && b[9] === 0x45) return 'webp';
|
|
71
|
+
// El %PDF- puede venir tras unos bytes de basura; el estándar tolera hasta
|
|
72
|
+
// 1024, y hay generadores que se aprovechan.
|
|
73
|
+
const cabeza = new TextDecoder('latin1').decode(b.subarray(0, Math.min(1024, b.length)));
|
|
74
|
+
if (cabeza.includes('%PDF-')) return 'pdf';
|
|
75
|
+
if (esZip(b)) return 'zip';
|
|
76
|
+
return 'otro';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ¿Esto es texto?
|
|
81
|
+
*
|
|
82
|
+
* Se decodifica en modo estricto, así un binario LANZA en vez de colarse como
|
|
83
|
+
* un reguero de caracteres de reemplazo. Y aun decodificando bien, se rechaza
|
|
84
|
+
* lo que trae bytes de control: hay binarios que pasan por UTF-8 válido y
|
|
85
|
+
* mandárselos a un modelo es gastar el encargo en basura.
|
|
86
|
+
*/
|
|
87
|
+
export function comoTexto(bytes: Uint8Array): string | null {
|
|
88
|
+
try {
|
|
89
|
+
const texto = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
90
|
+
for (const ch of texto) {
|
|
91
|
+
const c = ch.codePointAt(0)!;
|
|
92
|
+
if (c < 0x20 && c !== 0x0a && c !== 0x0d && c !== 0x09) return null;
|
|
93
|
+
}
|
|
94
|
+
return texto;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* El texto de un `.docx`.
|
|
102
|
+
*
|
|
103
|
+
* Un .docx es un ZIP con `word/document.xml` dentro. No hace falta entender
|
|
104
|
+
* Word: cada `</w:p>` cierra un párrafo y cada `<w:t>` envuelve un trozo de
|
|
105
|
+
* texto. Se quitan las etiquetas y quedan las palabras.
|
|
106
|
+
*
|
|
107
|
+
* Los `<w:t>` se conservan PEGADOS entre sí a propósito: Word parte una misma
|
|
108
|
+
* frase en varios cuando cambia algo del formato, y meter espacios entre ellos
|
|
109
|
+
* escribiría «contra tante» donde pone «contratante».
|
|
110
|
+
*/
|
|
111
|
+
export function textoDeDocx(bytes: Uint8Array): string | null {
|
|
112
|
+
const doc = leerZip(bytes).find((e) => e.nombre === 'word/document.xml');
|
|
113
|
+
if (!doc) return null;
|
|
114
|
+
const xml = comoTexto(doc.bytes);
|
|
115
|
+
if (!xml) return null;
|
|
116
|
+
return xml
|
|
117
|
+
.replace(/<w:p\b[^>]*\/>/g, '\n')
|
|
118
|
+
.replace(/<\/w:p>/g, '\n')
|
|
119
|
+
.replace(/<w:tab\b[^>]*\/>/g, '\t')
|
|
120
|
+
.replace(/<w:br\b[^>]*\/>/g, '\n')
|
|
121
|
+
.replace(/<[^>]+>/g, '')
|
|
122
|
+
.replace(/</g, '<')
|
|
123
|
+
.replace(/>/g, '>')
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/"/g, '"')
|
|
126
|
+
.replace(/'/g, "'")
|
|
127
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
128
|
+
.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `A1` → 0, `B1` → 1, `AA7` → 26. Sirve para no descolocar una fila con huecos. */
|
|
132
|
+
function columnaDe(ref: string): number {
|
|
133
|
+
const letras = /^([A-Z]+)/.exec(ref.toUpperCase())?.[1];
|
|
134
|
+
if (!letras) return 0;
|
|
135
|
+
let n = 0;
|
|
136
|
+
for (const c of letras) n = n * 26 + (c.charCodeAt(0) - 64);
|
|
137
|
+
return n - 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Lo que el XML trae escapado, de vuelta a texto. */
|
|
141
|
+
function desescapar(s: string): string {
|
|
142
|
+
return s
|
|
143
|
+
.replace(/</g, '<')
|
|
144
|
+
.replace(/>/g, '>')
|
|
145
|
+
.replace(/"/g, '"')
|
|
146
|
+
.replace(/'/g, "'")
|
|
147
|
+
.replace(/&#(\d+);/g, (_, d: string) => String.fromCodePoint(Number(d)))
|
|
148
|
+
.replace(/&/g, '&');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* El contenido de un `.xlsx`, como filas de texto.
|
|
153
|
+
*
|
|
154
|
+
* Un Excel es otro ZIP con XML, como el .docx, pero con una vuelta de tuerca:
|
|
155
|
+
* las celdas de texto no suelen guardar el texto. Guardan un ÍNDICE a
|
|
156
|
+
* `sharedStrings.xml`, donde cada cadena aparece una sola vez aunque se repita
|
|
157
|
+
* en mil celdas. Sin resolver esa tabla, una hoja llena de nombres se lee como
|
|
158
|
+
* una lista de números.
|
|
159
|
+
*
|
|
160
|
+
* Y hay cuatro maneras de que una celda tenga texto, según quién escribiera el
|
|
161
|
+
* archivo: `t="s"` (la tabla), `t="str"` (resultado de fórmula), `t="inlineStr"`
|
|
162
|
+
* (el texto ahí mismo) y sin `t` (un número). Se contemplan las cuatro porque
|
|
163
|
+
* Excel, LibreOffice y las librerías no escogen la misma.
|
|
164
|
+
*
|
|
165
|
+
* Las filas salen separadas por tabuladores: es lo que un modelo lee como
|
|
166
|
+
* tabla sin tener que adivinar dónde acaba una celda, y no exige entrecomillar
|
|
167
|
+
* nada.
|
|
168
|
+
*/
|
|
169
|
+
export function textoDeXlsx(bytes: Uint8Array): string | null {
|
|
170
|
+
const partes = leerZip(bytes);
|
|
171
|
+
const hojas = partes
|
|
172
|
+
.filter((e) => /^xl\/worksheets\/sheet\d*\.xml$/.test(e.nombre))
|
|
173
|
+
.sort((a, b) => a.nombre.localeCompare(b.nombre));
|
|
174
|
+
if (hojas.length === 0) return null;
|
|
175
|
+
|
|
176
|
+
// La tabla de cadenas compartidas, si la hay.
|
|
177
|
+
const compartidas: string[] = [];
|
|
178
|
+
const tabla = partes.find((e) => e.nombre === 'xl/sharedStrings.xml');
|
|
179
|
+
if (tabla) {
|
|
180
|
+
const xml = comoTexto(tabla.bytes) ?? '';
|
|
181
|
+
for (const si of xml.match(/<si>[\s\S]*?<\/si>/g) ?? []) {
|
|
182
|
+
// Una cadena puede venir partida en varios <t> cuando lleva formato
|
|
183
|
+
// dentro; se pegan sin separador, como en Word.
|
|
184
|
+
compartidas.push(desescapar([...si.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((m) => m[1] ?? '').join('')));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const salida: string[] = [];
|
|
189
|
+
for (const hoja of hojas) {
|
|
190
|
+
const xml = comoTexto(hoja.bytes);
|
|
191
|
+
if (!xml) continue;
|
|
192
|
+
const filas: string[] = [];
|
|
193
|
+
|
|
194
|
+
for (const fila of xml.match(/<row[^>]*>[\s\S]*?<\/row>/g) ?? []) {
|
|
195
|
+
const celdas: string[] = [];
|
|
196
|
+
for (const m of fila.matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
|
|
197
|
+
const attrs = m[1] ?? '';
|
|
198
|
+
const cuerpo = m[2] ?? '';
|
|
199
|
+
const tipo = /\bt="([^"]+)"/.exec(attrs)?.[1];
|
|
200
|
+
const ref = /\br="([^"]+)"/.exec(attrs)?.[1] ?? '';
|
|
201
|
+
|
|
202
|
+
let valor = '';
|
|
203
|
+
if (tipo === 'inlineStr') {
|
|
204
|
+
valor = desescapar([...cuerpo.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((x) => x[1] ?? '').join(''));
|
|
205
|
+
} else {
|
|
206
|
+
const v = /<v[^>]*>([\s\S]*?)<\/v>/.exec(cuerpo)?.[1] ?? '';
|
|
207
|
+
if (tipo === 's') valor = compartidas[Number(v)] ?? '';
|
|
208
|
+
else if (tipo === 'b') valor = v === '1' ? 'VERDADERO' : 'FALSO';
|
|
209
|
+
else valor = desescapar(v);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Una fila puede saltarse columnas: `A1` y luego `D1`. Colocar cada
|
|
213
|
+
// valor en SU columna es lo que evita que una tabla con huecos salga
|
|
214
|
+
// desplazada y el modelo lea el dato de otra cabecera.
|
|
215
|
+
const col = ref ? columnaDe(ref) : celdas.length;
|
|
216
|
+
while (celdas.length < col) celdas.push('');
|
|
217
|
+
celdas[col] = valor;
|
|
218
|
+
}
|
|
219
|
+
if (celdas.some((c) => c !== '')) filas.push(celdas.join('\t'));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (filas.length > 0) {
|
|
223
|
+
salida.push(hojas.length > 1 ? `[hoja ${salida.length + 1}]\n${filas.join('\n')}` : filas.join('\n'));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return salida.length > 0 ? salida.join('\n\n') : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* El texto de un PDF.
|
|
232
|
+
*
|
|
233
|
+
* Se apoya en `unpdf`, que es el motor de pdf.js empaquetado. Se hizo así tras
|
|
234
|
+
* medir: un extractor a mano lee bien los PDF que genera este mismo agente y
|
|
235
|
+
* se atraganta con los de Word o Chrome, que son justo los que manda un
|
|
236
|
+
* cliente. Las codificaciones de fuente son el problema, y resolverlas es lo
|
|
237
|
+
* que hace pdf.js.
|
|
238
|
+
*
|
|
239
|
+
* Devuelve null si no sale texto: un PDF escaneado es una imagen dentro de un
|
|
240
|
+
* PDF, y sin OCR no hay nada que leer. Decirlo es mejor que entregar el vacío.
|
|
241
|
+
*/
|
|
242
|
+
export async function textoDePdf(bytes: Uint8Array): Promise<string | null> {
|
|
243
|
+
try {
|
|
244
|
+
const { extractText, getDocumentProxy } = await import('unpdf');
|
|
245
|
+
const doc = await getDocumentProxy(bytes);
|
|
246
|
+
const { text } = await extractText(doc, { mergePages: true });
|
|
247
|
+
const limpio = (Array.isArray(text) ? text.join('\n') : text).trim();
|
|
248
|
+
return limpio.length > 0 ? limpio : null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Recorta y avisa de que ha recortado, que es lo que evita una cita a medias. */
|
|
255
|
+
function acotar(texto: string, tope: number): string {
|
|
256
|
+
if (texto.length <= tope) return texto;
|
|
257
|
+
return `${texto.slice(0, tope)}\n--- (recortado: son ${texto.length} caracteres) ---`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Abre todos los adjuntos de un encargo.
|
|
262
|
+
*
|
|
263
|
+
* Es `async` porque leer un PDF lo es. Los agentes ya trabajaban en `async`,
|
|
264
|
+
* así que lo único que cambia para quien llama es un `await`.
|
|
265
|
+
*/
|
|
266
|
+
export async function leerAdjuntos(adjuntos: AdjuntoRecibido[]): Promise<AdjuntosLeidos> {
|
|
267
|
+
const partes: string[] = [];
|
|
268
|
+
const imagenes: ImagenAdjunta[] = [];
|
|
269
|
+
let gastado = 0;
|
|
270
|
+
|
|
271
|
+
const anadir = (cabecera: string, cuerpo: string): void => {
|
|
272
|
+
const queda = MAX_CHARS_TOTAL - gastado;
|
|
273
|
+
if (queda <= 200) return;
|
|
274
|
+
const trozo = acotar(cuerpo, Math.min(MAX_CHARS_POR_ADJUNTO, queda));
|
|
275
|
+
gastado += trozo.length;
|
|
276
|
+
partes.push(`--- ${cabecera} ---\n${trozo}\n--- fin ---`);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const noSePudo = (a: AdjuntoRecibido, motivo: string): void => {
|
|
280
|
+
partes.push(
|
|
281
|
+
`[Archivo adjunto "${a.name}"${a.mime ? ` (${a.mime})` : ''}: ${motivo}. ` +
|
|
282
|
+
`Dilo con claridad en la respuesta en vez de ignorarlo.]`,
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (const a of adjuntos) {
|
|
287
|
+
const tipo = tipoDe(a.bytes);
|
|
288
|
+
|
|
289
|
+
if (tipo in MIMES_IMAGEN) {
|
|
290
|
+
// La imagen no se describe: se le enseña. El agente decide si su modelo
|
|
291
|
+
// puede mirarla; aquí sólo se separa de lo que es texto.
|
|
292
|
+
imagenes.push({ mime: MIMES_IMAGEN[tipo]!, bytes: a.bytes });
|
|
293
|
+
partes.push(`[Imagen adjunta "${a.name}": va con este mensaje, míralo.]`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (tipo === 'pdf') {
|
|
298
|
+
const texto = await textoDePdf(a.bytes);
|
|
299
|
+
if (texto) anadir(`PDF adjunto: ${a.name}`, texto);
|
|
300
|
+
else noSePudo(a, 'es un PDF del que no se pudo sacar texto (probablemente escaneado)');
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (tipo === 'zip') {
|
|
305
|
+
// Word y Excel son ZIPs también, así que se prueban antes de tratarlo
|
|
306
|
+
// como una carpeta: leer un .xlsx entrada por entrada devolvería su XML
|
|
307
|
+
// en crudo, que para un modelo es ruido caro.
|
|
308
|
+
const docx = textoDeDocx(a.bytes);
|
|
309
|
+
if (docx) {
|
|
310
|
+
anadir(`Documento adjunto: ${a.name}`, docx);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const xlsx = textoDeXlsx(a.bytes);
|
|
314
|
+
if (xlsx) {
|
|
315
|
+
anadir(`Hoja de cálculo adjunta (columnas separadas por tabulador): ${a.name}`, xlsx);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Un ZIP normal: una carpeta. Se leen los que sí sean texto y se
|
|
319
|
+
// NOMBRAN los que no, para que el modelo sepa qué había dentro.
|
|
320
|
+
const dentro = leerZip(a.bytes).slice(0, MAX_DENTRO_DEL_ZIP);
|
|
321
|
+
if (dentro.length === 0) {
|
|
322
|
+
noSePudo(a, 'es un archivo comprimido que no se pudo abrir');
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const ilegibles: string[] = [];
|
|
326
|
+
for (const e of dentro) {
|
|
327
|
+
const texto = comoTexto(e.bytes);
|
|
328
|
+
if (texto === null) ilegibles.push(e.nombre);
|
|
329
|
+
else anadir(`${a.name} → ${e.nombre}`, texto);
|
|
330
|
+
}
|
|
331
|
+
if (ilegibles.length > 0) {
|
|
332
|
+
partes.push(`[Dentro de "${a.name}" hay ${ilegibles.length} archivo(s) que no son texto: ${ilegibles.join(', ')}.]`);
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const texto = comoTexto(a.bytes);
|
|
338
|
+
if (texto !== null) anadir(`Archivo adjunto: ${a.name}`, texto);
|
|
339
|
+
else noSePudo(a, 'no es texto y este agente no puede abrirlo');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { texto: partes.join('\n\n'), imagenes };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* La imagen en el formato que espera `/chat/completions`.
|
|
347
|
+
*
|
|
348
|
+
* Vive aquí y no en cada agente porque los cuatro la necesitan igual, y una
|
|
349
|
+
* data-url mal montada falla con un error del proveedor que no dice nada.
|
|
350
|
+
*/
|
|
351
|
+
export function parteDeImagen(img: ImagenAdjunta): {
|
|
352
|
+
type: 'image_url';
|
|
353
|
+
image_url: { url: string };
|
|
354
|
+
} {
|
|
355
|
+
return {
|
|
356
|
+
type: 'image_url',
|
|
357
|
+
image_url: { url: `data:${img.mime};base64,${Buffer.from(img.bytes).toString('base64')}` },
|
|
358
|
+
};
|
|
359
|
+
}
|