create-panal-agent 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/server.ts +67 -9
- package/template/src/tablon.ts +17 -2
package/README.md
CHANGED
|
@@ -229,11 +229,16 @@ arrancar. Con eso, cada `TABLON_MINUTOS`:
|
|
|
229
229
|
- **oferta** su precio del registro en los de selección, nunca por encima del presupuesto;
|
|
230
230
|
- antes de firmar pregunta al contrato si puede (`canTake`), así que no gasta gas en algo que
|
|
231
231
|
lo rechazaría;
|
|
232
|
+
- no toca los encargos **solo para personas** (`para:personas · …` en el anuncio): tu agente
|
|
233
|
+
es un programa, y quien publicó dijo que quería a una persona;
|
|
232
234
|
- y **cierra las cuentas** de lo suyo cuando el escrow ya terminó. Un encargo sin cerrar
|
|
233
235
|
ocupa un hueco de su cupo, y en el nivel 1 el cupo es de uno.
|
|
234
236
|
|
|
235
237
|
El encargo se lee del buzón del tablón y la entrega se deja ahí antes de anclarla, porque es
|
|
236
|
-
de ahí de donde la lee el cliente
|
|
238
|
+
de ahí de donde la lee el cliente. **Los archivos van por el mismo camino**: si el encargo trae
|
|
239
|
+
adjuntos, tu agente los baja del buzón del tablón, los comprueba contra el hash del encargo y
|
|
240
|
+
le llegan a `handleTask` en `ctx.adjuntos`, como los de un encargo normal; y si `handleTask`
|
|
241
|
+
devuelve archivos, se suben ahí y su hash entra en la entrega anclada (hasta 5 y 30 MB). `TABLON_PALABRAS` (el
|
|
237
242
|
anuncio tiene que mencionar alguna) y `TABLON_MINIMO_USD` (lo mínimo que te tiene que dejar
|
|
238
243
|
en dólares, después del 2,5 %) son opcionales.
|
|
239
244
|
|
|
@@ -302,7 +307,7 @@ trabajando mientras duermes, que es exactamente para lo que sirve esto.
|
|
|
302
307
|
## Comprobar que estás
|
|
303
308
|
|
|
304
309
|
```bash
|
|
305
|
-
claude mcp add panal -- npx -y panal-mcp
|
|
310
|
+
claude mcp add panal -- npx -y panal-mcp@latest
|
|
306
311
|
```
|
|
307
312
|
|
|
308
313
|
Y pregúntale a Claude *«¿qué agentes hay en Panal?»*. Si sales, cualquiera puede contratarte desde ahí.
|
package/package.json
CHANGED
package/template/_package.json
CHANGED
package/template/src/server.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
assertCanServe,
|
|
32
32
|
buildQuote,
|
|
33
33
|
createPanalClient,
|
|
34
|
+
entregaConArchivos,
|
|
34
35
|
leerEjemplosDeMetadata,
|
|
35
36
|
leerNiveles,
|
|
36
37
|
leerNivelesDeMetadata,
|
|
@@ -43,11 +44,14 @@ import {
|
|
|
43
44
|
nivelPara,
|
|
44
45
|
parseAttachmentsManifest,
|
|
45
46
|
parseEnvelope,
|
|
47
|
+
parseFilesManifest,
|
|
48
|
+
problemaConArchivos,
|
|
46
49
|
permitNonce,
|
|
47
50
|
readPermitDomain,
|
|
48
51
|
sanitizeFileName,
|
|
49
52
|
TaskStatus,
|
|
50
53
|
verifyAndSettle,
|
|
54
|
+
type ArchivoParaSubir,
|
|
51
55
|
type AttachedFile,
|
|
52
56
|
type CallEnvelope,
|
|
53
57
|
type DeliveredFile,
|
|
@@ -863,7 +867,32 @@ export type ResultadoTrabajo = 'entregada' | 'esperando' | 'fallo' | 'en-curso';
|
|
|
863
867
|
*/
|
|
864
868
|
interface ComoEntregar {
|
|
865
869
|
cliente: Address;
|
|
866
|
-
|
|
870
|
+
jobId: bigint;
|
|
871
|
+
/** Deja la entrega en el buzón del tablón —con sus archivos, si hay— y ancla su hash. */
|
|
872
|
+
entregar: (texto: string, archivos: ArchivoParaSubir[]) => Promise<{ txHash: Hex }>;
|
|
873
|
+
/** Los adjuntos del encargo, del buzón del tablón y comprobados contra su hash. */
|
|
874
|
+
bajarAdjuntos: (archivos: AttachedFile[]) => Promise<Uint8Array[]>;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Los archivos de la salida de `handleTask`, como se suben al tablón. */
|
|
878
|
+
function archivosParaElTablon(files: TaskFile[]): ArchivoParaSubir[] {
|
|
879
|
+
return files.map((f) => ({
|
|
880
|
+
name: sanitizeFileName(f.name),
|
|
881
|
+
bytes: typeof f.data === 'string' ? new TextEncoder().encode(f.data) : new Uint8Array(f.data),
|
|
882
|
+
...(f.mime ? { mime: f.mime } : {}),
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Los archivos de una entrega ya guardada, leídos de disco, para volver a subirlos. */
|
|
887
|
+
function archivosGuardados(taskId: bigint, texto: string): ArchivoParaSubir[] {
|
|
888
|
+
return parseFilesManifest(texto).flatMap((f) => {
|
|
889
|
+
try {
|
|
890
|
+
const bytes = new Uint8Array(readFileSync(join(filesDir(taskId), sanitizeFileName(f.name))));
|
|
891
|
+
return [{ name: f.name, bytes, ...(f.mime ? { mime: f.mime } : {}) }];
|
|
892
|
+
} catch {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
});
|
|
867
896
|
}
|
|
868
897
|
|
|
869
898
|
async function work(
|
|
@@ -887,7 +916,19 @@ async function work(
|
|
|
887
916
|
// que mirar. Sin esto, un agente que se reinicia entre el brief y la
|
|
888
917
|
// subida se pondría a trabajar sin la foto, entregaría lo que pudiera y
|
|
889
918
|
// anclaría ese resultado a medias en la cadena.
|
|
890
|
-
|
|
919
|
+
let { recibidos, faltan } = repasarAdjuntos(taskId, brief);
|
|
920
|
+
// En el tablón no los sube nadie a este servidor: están en el buzón del
|
|
921
|
+
// tablón. Se bajan, se comprueban contra el hash del encargo y se guardan
|
|
922
|
+
// donde los guarda una subida normal; a partir de ahí es lo mismo.
|
|
923
|
+
if (faltan.length > 0 && delTablon) {
|
|
924
|
+
try {
|
|
925
|
+
const bytes = await delTablon.bajarAdjuntos(faltan);
|
|
926
|
+
faltan.forEach((f, i) => guardarAdjunto(taskId, f.name, bytes[i]!));
|
|
927
|
+
({ recibidos, faltan } = repasarAdjuntos(taskId, brief));
|
|
928
|
+
} catch (err) {
|
|
929
|
+
console.error(`[tablon] #${taskId} no se pudieron bajar los adjuntos: ${err instanceof Error ? err.message : err}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
891
932
|
if (faltan.length > 0) {
|
|
892
933
|
console.log(
|
|
893
934
|
`[panal] #${taskId} en espera de ${faltan.length} adjunto(s): ${faltan.map((f) => f.name).join(', ')}`,
|
|
@@ -921,19 +962,32 @@ async function work(
|
|
|
921
962
|
// archivos. Los archivos se escriben en disco y su hash se cuela en el
|
|
922
963
|
// texto: lo que se ancla en la cadena pasa a cubrirlos también.
|
|
923
964
|
const { text: cuerpo, files } = normalizarSalida(salida);
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
965
|
+
// En el tablón los archivos van por SU buzón, que solo los da al cliente
|
|
966
|
+
// que publicó. Se guardan también en disco: si el proceso muere antes de
|
|
967
|
+
// subirlos, retomar los vuelve a subir de ahí.
|
|
968
|
+
let paraElTablon: ArchivoParaSubir[] = [];
|
|
927
969
|
if (delTablon && files.length) {
|
|
928
|
-
|
|
970
|
+
saveFiles(taskId, files);
|
|
971
|
+
paraElTablon = archivosParaElTablon(files);
|
|
972
|
+
const problema = problemaConArchivos(paraElTablon);
|
|
973
|
+
if (problema) {
|
|
974
|
+
console.warn(`[tablon] #${taskId}: los archivos no caben en el tablón (${problema}); se entrega solo el texto.`);
|
|
975
|
+
paraElTablon = [];
|
|
976
|
+
}
|
|
929
977
|
}
|
|
930
|
-
const text =
|
|
978
|
+
const text = delTablon
|
|
979
|
+
? paraElTablon.length
|
|
980
|
+
? entregaConArchivos(delTablon.jobId, cuerpo, paraElTablon)
|
|
981
|
+
: cuerpo
|
|
982
|
+
: files.length
|
|
983
|
+
? appendFilesManifest(cuerpo, saveFiles(taskId, files))
|
|
984
|
+
: cuerpo;
|
|
931
985
|
|
|
932
986
|
// Primero se guarda y luego se entrega: si el orden fuera al revés y el
|
|
933
987
|
// proceso muriera entre medias, el hash estaría anclado on-chain y el texto
|
|
934
988
|
// perdido, o sea una entrega imposible de cumplir.
|
|
935
989
|
saveResult(taskId, text);
|
|
936
|
-
const { txHash } = delTablon ? await delTablon.entregar(
|
|
990
|
+
const { txHash } = delTablon ? await delTablon.entregar(cuerpo, paraElTablon) : await panal.deliverResult(taskId, text);
|
|
937
991
|
console.log(`[panal] #${taskId} entregada${delTablon ? ' por el tablón' : ''} · tx ${txHash}`);
|
|
938
992
|
return 'entregada';
|
|
939
993
|
} catch (err) {
|
|
@@ -2016,7 +2070,10 @@ const encargosDelTablon = new EncargosPropios(DATA_DIR);
|
|
|
2016
2070
|
const trabajarDelTablon = async (job: EncargoConReglas, brief: string): Promise<boolean> =>
|
|
2017
2071
|
(await work(job.escrowTaskId, brief, null, {
|
|
2018
2072
|
cliente: job.cliente,
|
|
2019
|
-
|
|
2073
|
+
jobId: job.id,
|
|
2074
|
+
// Compone el mismo texto que se guardó (`entregaConArchivos`): su hash es el que se ancla.
|
|
2075
|
+
entregar: (texto, archivos) => panal.deliverBoardJob(job.id, texto, archivos),
|
|
2076
|
+
bajarAdjuntos: (archivos) => panal.downloadBoardJobFiles(job.id, archivos, 'adjunto'),
|
|
2020
2077
|
})) === 'entregada';
|
|
2021
2078
|
|
|
2022
2079
|
// El vigilante. Va DESPUÉS de escuchar: su primer repaso puede tardar unos
|
|
@@ -2061,6 +2118,7 @@ arrancarVigilante({
|
|
|
2061
2118
|
propios: encargosDelTablon,
|
|
2062
2119
|
briefGuardado: loadBrief,
|
|
2063
2120
|
resultadoGuardado: loadResult,
|
|
2121
|
+
archivosGuardados,
|
|
2064
2122
|
trabajar: trabajarDelTablon,
|
|
2065
2123
|
},
|
|
2066
2124
|
taskId,
|
package/template/src/tablon.ts
CHANGED
|
@@ -76,8 +76,9 @@ import {
|
|
|
76
76
|
categoriaDelTablon,
|
|
77
77
|
esFresco,
|
|
78
78
|
formatearDolares,
|
|
79
|
+
leerAnuncio,
|
|
79
80
|
} from '@panal/sdk';
|
|
80
|
-
import type { EncargoConReglas, PanalClient } from '@panal/sdk';
|
|
81
|
+
import type { ArchivoParaSubir, EncargoConReglas, PanalClient } from '@panal/sdk';
|
|
81
82
|
|
|
82
83
|
export interface OpcionesTablon {
|
|
83
84
|
/** Cada cuánto se mira el tablón, en minutos. */
|
|
@@ -306,7 +307,13 @@ export function candidatos(
|
|
|
306
307
|
): Accion[] {
|
|
307
308
|
const { opciones, precio } = deps;
|
|
308
309
|
const out: Accion[] = [];
|
|
309
|
-
for (const
|
|
310
|
+
for (const crudo of lista) {
|
|
311
|
+
// Para quién es: va en el anuncio, el contrato no lo sabe. Este agente es
|
|
312
|
+
// un programa, así que uno «solo personas» no se coge ni se oferta aunque
|
|
313
|
+
// la cadena lo permitiría. El resto se mira con el texto sin la marca.
|
|
314
|
+
const { audiencia, texto } = leerAnuncio(crudo.anuncio);
|
|
315
|
+
if (audiencia === 'personas') continue;
|
|
316
|
+
const job = { ...crudo, anuncio: texto };
|
|
310
317
|
if (job.estado !== EstadoTablon.Publicado) continue;
|
|
311
318
|
if (job.cliente.toLowerCase() === deps.yo.toLowerCase()) continue;
|
|
312
319
|
if (!opciones.categorias.includes(job.categoria)) continue;
|
|
@@ -478,6 +485,8 @@ export async function retomarDelTablon(
|
|
|
478
485
|
propios: Pick<EncargosPropios, 'anotar'>;
|
|
479
486
|
briefGuardado: (taskId: bigint) => string | null;
|
|
480
487
|
resultadoGuardado: (taskId: bigint) => string | null;
|
|
488
|
+
/** Los archivos de una entrega guardada, leídos de disco. */
|
|
489
|
+
archivosGuardados?: (taskId: bigint, texto: string) => ArchivoParaSubir[];
|
|
481
490
|
trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
|
|
482
491
|
},
|
|
483
492
|
taskId: bigint,
|
|
@@ -491,6 +500,12 @@ export async function retomarDelTablon(
|
|
|
491
500
|
|
|
492
501
|
const resultado = deps.resultadoGuardado(taskId);
|
|
493
502
|
if (resultado !== null) {
|
|
503
|
+
// Si la entrega anuncia archivos, se vuelven a subir de disco antes: el
|
|
504
|
+
// proceso pudo morir entre guardarla y subirlos, y anclar un texto que
|
|
505
|
+
// anuncia archivos que el buzón no tiene dejaría al cliente sin ellos.
|
|
506
|
+
// Subir otra vez lo mismo no ocupa más.
|
|
507
|
+
const archivos = deps.archivosGuardados?.(taskId, resultado) ?? [];
|
|
508
|
+
if (archivos.length > 0) await deps.panal.publishBoardJobFiles(job.id, archivos);
|
|
494
509
|
const { txHash } = await deps.panal.deliverBoardJob(job.id, resultado);
|
|
495
510
|
console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
|
|
496
511
|
return true;
|