354 lines
17 KiB
JSON
354 lines
17 KiB
JSON
{
|
|
"name": "PymesBot — Coding Loop HTTP Native",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [{"field": "minutes", "minutesInterval": 5}]
|
|
}
|
|
},
|
|
"id": "cron-trigger",
|
|
"name": "⏰ Cron cada 5 min",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [0, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/tasks.json",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "httpHeaderAuth",
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "http-read-tasks",
|
|
"name": "📋 HTTP: Leer tasks.json",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [220, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const response = $input.item.json;\nconst content = Buffer.from(response.content, 'base64').toString('utf-8');\nconst tasks = JSON.parse(content);\n\nreturn [{\n json: {\n tasks: tasks.tareas,\n key_pool: tasks.key_pool,\n config: tasks.config,\n file_sha: response.sha,\n raw_tasks: tasks\n }\n}];"
|
|
},
|
|
"id": "parse-tasks",
|
|
"name": "📝 Parsear tasks.json",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [440, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tasks = $input.item.json.tasks;\nconst key_pool = $input.item.json.key_pool;\nconst file_sha = $input.item.json.file_sha;\nconst raw_tasks = $input.item.json.raw_tasks;\n\nconst completadas = new Set(tasks.filter(t => t.status === 'done').map(t => t.id));\n\nconst candidatas = tasks\n .filter(t => {\n if (t.status !== 'pending') return false;\n if (t.intentos >= 3) return false;\n const depsPendientes = (t.dependencias || []).filter(dep => !completadas.has(dep));\n return depsPendientes.length === 0;\n })\n .sort((a, b) => {\n if (a.sprint !== b.sprint) return a.sprint - b.sprint;\n return a.prioridad - b.prioridad;\n });\n\nif (candidatas.length === 0) {\n const pendientes = tasks.filter(t => t.status === 'pending').length;\n return [{\n json: {\n hay_tarea: false,\n motivo: pendientes === 0 ? 'TODAS_COMPLETADAS' : 'DEPENDENCIAS_BLOQUEADAS',\n stats: { total: tasks.length, completadas: completadas.size, pendientes }\n }\n }];\n}\n\nconst tarea = candidatas[0];\n\nconst ahora = Date.now();\nconst keysDisponibles = key_pool\n .filter(k => k.disponible)\n .filter(k => {\n if (!k.ultimo_uso) return true;\n const elapsed = (ahora - new Date(k.ultimo_uso).getTime()) / 1000;\n return elapsed >= k.cooldown_segundos;\n })\n .sort((a, b) => {\n if (!a.ultimo_uso) return -1;\n if (!b.ultimo_uso) return 1;\n return new Date(a.ultimo_uso) - new Date(b.ultimo_uso);\n });\n\nlet keyElegida = keysDisponibles.find(k => k.servicio === tarea.modelo_preferido);\nif (!keyElegida) keyElegida = keysDisponibles[0];\n\nif (!keyElegida) {\n return [{\n json: {\n hay_tarea: false,\n motivo: 'TODAS_KEYS_EN_COOLDOWN',\n stats: { total: tasks.length, completadas: completadas.size }\n }\n }];\n}\n\nreturn [{\n json: {\n hay_tarea: true,\n tarea,\n key_elegida: keyElegida,\n tasks_all: tasks,\n key_pool,\n file_sha,\n raw_tasks\n }\n}];"
|
|
},
|
|
"id": "select-task",
|
|
"name": "🎯 Seleccionar próxima tarea",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [660, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {"caseSensitive": true},
|
|
"conditions": [
|
|
{
|
|
"id": "check-hay-tarea",
|
|
"leftValue": "={{ $json.hay_tarea }}",
|
|
"rightValue": true,
|
|
"operator": {"type": "boolean", "operation": "equals"}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
"id": "if-hay-tarea",
|
|
"name": "¿Hay tarea disponible?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [880, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "return [{ json: { url: `https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/specs/${$json.tarea.proyecto === 'pymesbot-installer' ? '02_PYMESBOT_INSTALLER_SPEC.md' : '01_PYMESBOT_PROJECT_SPEC.md'}` } }];"
|
|
},
|
|
"id": "build-spec-url",
|
|
"name": "🔗 Build Spec URL",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1100, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{ $json.url }}",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "httpHeaderAuth",
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "http-read-spec",
|
|
"name": "📖 HTTP: Leer spec",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [1320, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tarea = $('🎯 Seleccionar próxima tarea').item.json.tarea;\nconst specResponse = $input.item.json;\nconst specContent = Buffer.from(specResponse.content, 'base64').toString('utf-8');\n\nreturn [{\n json: {\n ...($('🎯 Seleccionar próxima tarea').item.json),\n spec_content: specContent\n }\n}];"
|
|
},
|
|
"id": "parse-spec",
|
|
"name": "📝 Parsear spec",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1540, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tarea = $('🎯 Seleccionar próxima tarea').item.json.tarea;\nconst spec_content = $input.item.json.spec_content;\nconst key_elegida = $('🎯 Seleccionar próxima tarea').item.json.key_elegida;\n\nconst archivoPrincipal = (tarea.archivos_a_crear && tarea.archivos_a_crear.length > 0)\n ? tarea.archivos_a_crear[0]\n : tarea.archivo_destino;\n\nconst prompt = `Sos un desarrollador Python/HTML/JS senior. Implementa código de producción.\n\n## TAREA\nID: ${tarea.id}\nTítulo: ${tarea.titulo}\nDescripción: ${tarea.descripcion}\n\n## ARCHIVO\n${archivoPrincipal}\n\n## CONTEXTO\n${spec_content}\n\n## REGLAS\n1. Respondé SÓLO con código. Sin markdown, sin explicaciones.\n2. Python 3.11+ válido con type hints.\n3. Comentarios en español.\n4. Código completo y funcional.\n\nCÓDIGO:`;\n\nconst GLM_KEY_1 = '6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS';\nconst MINIMAX_KEY_1 = 'sk-cp-XC8cbgbVBuv1g8mMcao0ABeZu_rGEN_S22EhBUqo4lJbY_UJVqUVO5XF8hVobp8gE_39JbgQggr00TQwNdV9vP458Y_MBC_8GstvzmwhuukEGY4a2I5_L6A';\n\nconst API_CONFIGS = {\n glm: { url: 'https://api.z.ai/api/paas/v4/chat/completions', model: 'glm-4.7', key: GLM_KEY_1 },\n minimax: { url: 'https://api.minimax.io/v1/chat/completions', model: 'MiniMax-M2.5', key: MINIMAX_KEY_1 }\n};\n\nconst apiConfig = API_CONFIGS[key_elegida.servicio];\n\nreturn [{\n json: {\n ...($('🎯 Seleccionar próxima tarea').item.json),\n prompt,\n archivo_objetivo: archivoPrincipal,\n api_url: apiConfig.url,\n api_model: apiConfig.model,\n api_key: apiConfig.key,\n key_elegida: key_elegida\n }\n}];"
|
|
},
|
|
"id": "build-prompt",
|
|
"name": "🔨 Construir prompt",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1760, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{ $json.api_url }}",
|
|
"authentication": "none",
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "model",
|
|
"value": "={{ $json.api_model }}"
|
|
},
|
|
{
|
|
"name": "messages",
|
|
"value": "={{ [{\"role\": \"system\", \"content\": \"Sos un desarrollador senior. Respondés SOLO con código. Sin explicaciones. Sin markdown.\"}, {\"role\": \"user\", \"content\": $json.prompt}] }}"
|
|
},
|
|
{
|
|
"name": "max_tokens",
|
|
"value": "4000"
|
|
},
|
|
{
|
|
"name": "temperature",
|
|
"value": "0.1"
|
|
}
|
|
]
|
|
},
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Bearer {{ $json.api_key }}"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "http-call-ai",
|
|
"name": "🤖 HTTP: Llamar a la IA",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [1980, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const response = $input.item.json;\nconst promptResponse = $input.item.json;\nlet codigo = promptResponse.choices?.[0]?.message?.content || '';\ncodigo = codigo.replace(/^```[a-z]*\\n?/gm, '').replace(/^```\\n?/gm, '').trim();\n\nreturn [{\n json: {\n ...($('🔨 Construir prompt').item.json),\n codigo_generado: codigo,\n key_usada: $('🔨 Construir prompt').item.json.key_elegida.id\n }\n}];"
|
|
},
|
|
"id": "parse-code",
|
|
"name": "📝 Parsear código",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [2200, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {"caseSensitive": true},
|
|
"conditions": [
|
|
{
|
|
"id": "check-codigo",
|
|
"leftValue": "={{ $json.codigo_generado }}",
|
|
"rightValue": "",
|
|
"operator": {"type": "string", "operation": "notEmpty"}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
}
|
|
},
|
|
"id": "if-codigo-ok",
|
|
"name": "¿Código generado?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [2420, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "return [{ json: { url: `https://gitea.cbcren.online/api/v1/repos/renato97/${$('🎯 Seleccionar próxima tarea').item.json.tarea.proyecto}/contents/${$('📝 Parsear código').item.json.archivo_objetivo}` } }];"
|
|
},
|
|
"id": "build-commit-url",
|
|
"name": "🔗 Build commit URL",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [2640, 100]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "PUT",
|
|
"url": "={{ $json.url }}",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "httpHeaderAuth",
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "message",
|
|
"value": "= [BOT/{{ $('📝 Parsear código').item.json.key_usada }}] {{ $('🎯 Seleccionar próxima tarea').item.json.tarea.id }} — {{ $('🎯 Seleccionar próxima tarea').item.json.tarea.titulo }}"
|
|
},
|
|
{
|
|
"name": "content",
|
|
"value": "={{ Buffer.from($('📝 Parsear código').item.json.codigo_generado).toString('base64') }}"
|
|
},
|
|
{
|
|
"name": "branch",
|
|
"value": "main"
|
|
}
|
|
]
|
|
},
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "http-commit",
|
|
"name": "📦 HTTP: Commitear a Gitea",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [2860, 100]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Actualizar tasks.json en Gitea marcar la tarea como done\nconst tareas = $('🎯 Seleccionar próxima tarea').item.json.tasks_all;\nconst key_pool = $('🎯 Seleccionar próxima tarea').item.json.key_pool;\nconst tarea = $('🎯 Seleccionar próxima tarea').item.json.tarea;\nconst file_sha = $('🎯 Seleccionar próxima tarea').item.json.file_sha;\nconst raw_tasks = $('🎯 Seleccionar próxima tarea').item.json.raw_tasks;\nconst key_usada = $('📝 Parsear código').item.json.key_usada;\n\nconst tareas_actualizadas = tareas.map(t => {\n if (t.id !== tarea.id) return t;\n return { ...t, status: 'done', intentos: t.intentos + 1, modelo_usado: key_usada, completada_el: new Date().toISOString() };\n});\n\nconst key_pool_actualizado = key_pool.map(k => {\n if (k.id !== key_usada) return k;\n return { ...k, ultimo_uso: new Date().toISOString(), usos_hoy: (k.usos_hoy || 0) + 1 };\n});\n\nconst nuevo_tasks_json = { ...raw_tasks, tareas: tareas_actualizadas, key_pool: key_pool_actualizado };\nconst contenidoBase64 = Buffer.from(JSON.stringify(nuevo_tasks_json, null, 2)).toString('base64');\n\nreturn [{\n json: {\n url: 'https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/tasks.json',\n method: 'PUT',\n body: {\n message: `[BOT] tasks.json — ${tarea.id} marcada como done`,\n content: contenidoBase64,\n sha: file_sha,\n branch: 'main'\n }\n }\n}];"
|
|
},
|
|
"id": "build-update-tasks",
|
|
"name": "🔨 Build update tasks.json",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [3080, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "PUT",
|
|
"url": "={{ $json.url }}",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "httpHeaderAuth",
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "body",
|
|
"value": "={{ JSON.stringify($json.body) }}"
|
|
}
|
|
]
|
|
},
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"
|
|
},
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "http-update-tasks",
|
|
"name": "✍️ HTTP: Actualizar tasks.json",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [3300, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage",
|
|
"sendBody": true,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "chat_id",
|
|
"value": "692714536"
|
|
},
|
|
{
|
|
"name": "text",
|
|
"value": "=Commit OK: {{ $('🎯 Seleccionar próxima tarea').item.json.tarea.id }} - {{ $('🎯 Seleccionar próxima tarea').item.json.tarea.titulo }}"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "tg-ok",
|
|
"name": "Telegram OK",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [3520, 80]
|
|
}
|
|
],
|
|
"connections": {
|
|
"⏰ Cron cada 5 min": {"main": [[{"node": "📋 HTTP: Leer tasks.json", "type": "main", "index": 0}]]},
|
|
"📋 HTTP: Leer tasks.json": {"main": [[{"node": "📝 Parsear tasks.json", "type": "main", "index": 0}]]},
|
|
"📝 Parsear tasks.json": {"main": [[{"node": "🎯 Seleccionar próxima tarea", "type": "main", "index": 0}]]},
|
|
"🎯 Seleccionar próxima tarea": {"main": [[{"node": "¿Hay tarea disponible?", "type": "main", "index": 0}]]},
|
|
"¿Hay tarea disponible?": {
|
|
"main": [[{"node": "🔗 Build Spec URL", "type": "main", "index": 0}]]
|
|
},
|
|
"🔗 Build Spec URL": {"main": [[{"node": "📖 HTTP: Leer spec", "type": "main", "index": 0}]]},
|
|
"📖 HTTP: Leer spec": {"main": [[{"node": "📝 Parsear spec", "type": "main", "index": 0}]]},
|
|
"📝 Parsear spec": {"main": [[{"node": "🔨 Construir prompt", "type": "main", "index": 0}]]},
|
|
"🔨 Construir prompt": {"main": [[{"node": "🤖 HTTP: Llamar a la IA", "type": "main", "index": 0}]]},
|
|
"🤖 HTTP: Llamar a la IA": {"main": [[{"node": "📝 Parsear código", "type": "main", "index": 0}]]},
|
|
"📝 Parsear código": {"main": [[{"node": "¿Código generado?", "type": "main", "index": 0}]]},
|
|
"¿Código generado?": {
|
|
"main": [[{"node": "🔗 Build commit URL", "type": "main", "index": 0}]]
|
|
},
|
|
"🔗 Build commit URL": {"main": [[{"node": "📦 HTTP: Commitear a Gitea", "type": "main", "index": 0}]]},
|
|
"📦 HTTP: Commitear a Gitea": {"main": [[{"node": "🔨 Build update tasks.json", "type": "main", "index": 0}]]},
|
|
"🔨 Build update tasks.json": {"main": [[{"node": "✍️ HTTP: Actualizar tasks.json", "type": "main", "index": 0}]]},
|
|
"✍️ HTTP: Actualizar tasks.json": {"main": [[{"node": "Telegram OK", "type": "main", "index": 0}]]}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": ["pymesbot"],
|
|
"meta": {"instanceId": "pymesbot-http-native"}
|
|
}
|