diff --git a/n8n_workflow_V5_LOOP_TELEGRAM.json b/n8n_workflow_V5_LOOP_TELEGRAM.json new file mode 100644 index 0000000..d219355 --- /dev/null +++ b/n8n_workflow_V5_LOOP_TELEGRAM.json @@ -0,0 +1,501 @@ +{ + "name": "PymesBot — Loop V5 con Telegram", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [{"field": "minutes", "minutesInterval": 5}] + } + }, + "id": "cron-trigger", + "name": "⏰ Inicio Loop", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [0, 500] + }, + { + "parameters": { + "url": "https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/tasks.json", + "authentication": "none", + "headerParameters": { + "parameters": [ + {"name": "Authorization", "value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"} + ] + }, + "options": {} + }, + "id": "http-read-tasks", + "name": "📥 Leer tasks.json", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [220, 500] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=📥 tasks.json leído"} + ] + }, + "options": {} + }, + "id": "tg-read-ok", + "name": "📢 TG: Leído", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [440, 580] + }, + { + "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 tareas: tasks.tareas,\n key_pool: tasks.key_pool,\n config: tasks.config,\n meta: tasks.meta,\n file_sha: response.sha,\n raw_tasks: tasks\n }\n}];" + }, + "id": "parse-tasks", + "name": "📝 Parsear JSON", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [660, 500] + }, + { + "parameters": { + "jsCode": "const tareas = $input.item.json.tareas;\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(tareas.filter(t => t.status === 'done').map(t => t.id));\n\nconst candidatas = tareas\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 = tareas.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: tareas.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: tareas.length, completadas: completadas.size }\n }\n }];\n}\n\nreturn [{\n json: {\n hay_tarea: true,\n tarea: tarea,\n key_elegida: keyElegida,\n tareas_all: tareas,\n key_pool: key_pool,\n file_sha: file_sha,\n raw_tasks: raw_tasks\n }\n}];" + }, + "id": "select-task", + "name": "🎯 Seleccionar tarea", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [880, 500] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=🎯 Tarea: {{ $json.hay_tarea ? $json.tarea.id + ' - ' + $json.tarea.titulo : 'NO disponible (' + $json.motivo + ')' }}"} + ] + }, + "options": {} + }, + "id": "tg-tarea-selected", + "name": "📢 TG: Tarea", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1100, 580] + }, + { + "parameters": { + "conditions": { + "options": {"caseSensitive": true}, + "conditions": [ + { + "id": "check-tarea", + "leftValue": "={{ $json.hay_tarea }}", + "rightValue": true, + "operator": {"type": "boolean", "operation": "equals"} + } + ], + "combinator": "and" + } + }, + "id": "if-hay-tarea", + "name": "¿Hay tarea?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1320, 500] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=⏸️ Esperando... {{ $json.motivo }}. Total: {{ $json.stats.total }} | Hechas: {{ $json.stats.completadas }}"} + ] + }, + "options": {} + }, + "id": "tg-no-tarea", + "name": "📢 TG: Espera", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1540, 650] + }, + { + "parameters": { + "jsCode": "// Esperar próximo ciclo\nreturn [{ json: { fin_ciclo: true, motivo: 'sin_tarea' } }];" + }, + "id": "wait-next", + "name": "⏳ Esperar 5min", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1760, 650] + }, + { + "parameters": { + "url": "={{ $('🎯 Seleccionar tarea').item.json.tarea.proyecto === 'pymesbot-installer' ? 'https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/specs/02_PYMESBOT_INSTALLER_SPEC.md' : 'https://gitea.cbcren.online/api/v1/repos/renato97/pymesbot-infra/contents/specs/01_PYMESBOT_PROJECT_SPEC.md' }}", + "authentication": "none", + "headerParameters": { + "parameters": [ + {"name": "Authorization", "value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"} + ] + }, + "options": {} + }, + "id": "http-read-spec", + "name": "📖 Leer spec", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1540, 300] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=📖 Spec leído"} + ] + }, + "options": {} + }, + "id": "tg-spec-ok", + "name": "📢 TG: Spec", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1760, 380] + }, + { + "parameters": { + "jsCode": "const tarea = $('🎯 Seleccionar tarea').item.json.tarea;\nconst specResponse = $input.item.json;\nconst specContent = Buffer.from(specResponse.content, 'base64').toString('utf-8');\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 limpio y completo.\n\n## TAREA\nID: ${tarea.id}\nTítulo: ${tarea.titulo}\nDescripción: ${tarea.descripcion}\n\n## ARCHIVO\n${archivoPrincipal}\n\n## CONTEXTO DEL SPEC\n${specContent}\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 key_elegida = $('🎯 Seleccionar tarea').item.json.key_elegida;\nconst apiConfig = API_CONFIGS[key_elegida.servicio];\n\nreturn [{\n json: {\n ...$('🎯 Seleccionar tarea').item.json,\n spec_content: specContent,\n prompt: prompt,\n archivo_objetivo: archivoPrincipal,\n api_url: apiConfig.url,\n api_model: apiConfig.model,\n api_key: apiConfig.key\n }\n}];" + }, + "id": "build-prompt", + "name": "🔨 Build prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1980, 300] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=🤖 Llamando a {{ $('🎯 Seleccionar tarea').item.json.key_elegida.servicio }} ({{ $json.api_model }})"} + ] + }, + "options": {} + }, + "id": "tg-ai-call", + "name": "📢 TG: Llamando IA", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [2200, 380] + }, + { + "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": "🤖 Llamar IA", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [2420, 300] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=✅ Código recibido ({{ $json.choices[0].message.content.length }} chars)"} + ] + }, + "options": {} + }, + "id": "tg-ai-response", + "name": "📢 TG: Respuesta IA", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [2640, 380] + }, + { + "parameters": { + "jsCode": "const response = $input.item.json;\nlet codigo = response.choices?.[0]?.message?.content || '';\ncodigo = codigo.replace(/^```[a-z]*\\n?/gm, '').replace(/^```\\n?/gm, '').trim();\n\nreturn [{\n json: {\n ...$('🔨 Build prompt').item.json,\n codigo_generado: codigo\n }\n}];" + }, + "id": "parse-code", + "name": "📝 Extraer código", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2860, 300] + }, + { + "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 OK?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [3080, 300] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=❌ Error: código vacío para {{ $('🎯 Seleccionar tarea').item.json.tarea.id }}"} + ] + }, + "options": {} + }, + "id": "tg-error-no-code", + "name": "📢 TG: Error código", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [3300, 400] + }, + { + "parameters": { + "method": "PUT", + "url": "={{ 'https://gitea.cbcren.online/api/v1/repos/renato97/' + $('🎯 Seleccionar tarea').item.json.tarea.proyecto + '/contents/' + $('📝 Extraer código').item.json.archivo_objetivo }}", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "message", "value": "= [BOT] {{ $('🎯 Seleccionar tarea').item.json.tarea.id }} — {{ $('🎯 Seleccionar tarea').item.json.tarea.titulo }}"}, + {"name": "content", "value": "={{ Buffer.from($('📝 Extraer código').item.json.codigo_generado).toString('base64') }}"}, + {"name": "branch", "value": "main"} + ] + }, + "headerParameters": { + "parameters": [ + {"name": "Authorization", "value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"} + ] + }, + "options": {} + }, + "id": "http-commit", + "name": "📦 Commit a Gitea", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [3300, 200] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=📦 Commit OK: {{ $('📝 Extraer código').item.json.archivo_objetivo }}"} + ] + }, + "options": {} + }, + "id": "tg-commit-ok", + "name": "📢 TG: Commit", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [3520, 280] + }, + { + "parameters": { + "jsCode": "const tareas = $('🎯 Seleccionar tarea').item.json.tareas_all;\nconst key_pool = $('🎯 Seleccionar tarea').item.json.key_pool;\nconst tarea = $('🎯 Seleccionar tarea').item.json.tarea;\nconst file_sha = $('🎯 Seleccionar tarea').item.json.file_sha;\nconst raw_tasks = $('🎯 Seleccionar tarea').item.json.raw_tasks;\nconst key_usada = $('🎯 Seleccionar tarea').item.json.key_elegida.id;\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] ${tarea.id} marcada como done`,\n content: contenidoBase64,\n sha: file_sha,\n branch: 'main'\n },\n tarea_id: tarea.id\n }\n}];" + }, + "id": "build-update", + "name": "🔨 Preparar update", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [3740, 200] + }, + { + "parameters": { + "method": "PUT", + "url": "={{ $json.url }}", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "body", "value": "={{ $json.body }}"} + ] + }, + "headerParameters": { + "parameters": [ + {"name": "Authorization", "value": "token efeed2af00597883adb04da70bd6a7c2993ae92d"}, + {"name": "Content-Type", "value": "application/json"} + ] + }, + "options": {} + }, + "id": "http-update-tasks", + "name": "✍️ Update tasks.json", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [3960, 200] + }, + { + "parameters": { + "url": "https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage", + "authentication": "none", + "sendBody": true, + "bodyParameters": { + "parameters": [ + {"name": "chat_id", "value": "692714536"}, + {"name": "text", "value": "=🔄 Tarea {{ $('🔨 Preparar update').item.json.tarea_id }} completada. Reiniciando loop..."} + ] + }, + "options": {} + }, + "id": "tg-success-loop", + "name": "📢 TG: Fin loop", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [4180, 280] + }, + { + "parameters": { + "jsCode": "// Pausa antes de reiniciar\nreturn [{ json: { loop_restart: true } }];" + }, + "id": "loop-restart", + "name": "🔄 Reiniciar", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [4400, 280] + } + ], + "connections": { + "⏰ Inicio Loop": { + "main": [[{"node": "📥 Leer tasks.json", "type": "main", "index": 0}]] + }, + "📥 Leer tasks.json": { + "main": [ + [{"node": "📢 TG: Leído", "type": "main", "index": 0}] + ] + }, + "📢 TG: Leído": { + "main": [[{"node": "📝 Parsear JSON", "type": "main", "index": 0}]] + }, + "📝 Parsear JSON": { + "main": [[{"node": "🎯 Seleccionar tarea", "type": "main", "index": 0}]] + }, + "🎯 Seleccionar tarea": { + "main": [[{"node": "📢 TG: Tarea", "type": "main", "index": 0}]] + }, + "📢 TG: Tarea": { + "main": [[{"node": "¿Hay tarea?", "type": "main", "index": 0}]] + }, + "¿Hay tarea?": { + "main": [ + [{"node": "📖 Leer spec", "type": "main", "index": 0}], + [{"node": "📢 TG: Espera", "type": "main", "index": 0}] + ] + }, + "📢 TG: Espera": { + "main": [[{"node": "⏳ Esperar 5min", "type": "main", "index": 0}]] + }, + "⏳ Esperar 5min": { + "main": [[{"node": "🔄 Reiniciar", "type": "main", "index": 0}]] + }, + "📖 Leer spec": { + "main": [[{"node": "📢 TG: Spec", "type": "main", "index": 0}]] + }, + "📢 TG: Spec": { + "main": [[{"node": "🔨 Build prompt", "type": "main", "index": 0}]] + }, + "🔨 Build prompt": { + "main": [[{"node": "📢 TG: Llamando IA", "type": "main", "index": 0}]] + }, + "📢 TG: Llamando IA": { + "main": [[{"node": "🤖 Llamar IA", "type": "main", "index": 0}]] + }, + "🤖 Llamar IA": { + "main": [[{"node": "📢 TG: Respuesta IA", "type": "main", "index": 0}]] + }, + "📢 TG: Respuesta IA": { + "main": [[{"node": "📝 Extraer código", "type": "main", "index": 0}]] + }, + "📝 Extraer código": { + "main": [[{"node": "¿Código OK?", "type": "main", "index": 0}]] + }, + "¿Código OK?": { + "main": [ + [{"node": "📦 Commit a Gitea", "type": "main", "index": 0}], + [{"node": "📢 TG: Error código", "type": "main", "index": 0}] + ] + }, + "📢 TG: Error código": { + "main": [[{"node": "🔄 Reiniciar", "type": "main", "index": 0}]] + }, + "📦 Commit a Gitea": { + "main": [[{"node": "📢 TG: Commit", "type": "main", "index": 0}]] + }, + "📢 TG: Commit": { + "main": [[{"node": "🔨 Preparar update", "type": "main", "index": 0}]] + }, + "🔨 Preparar update": { + "main": [[{"node": "✍️ Update tasks.json", "type": "main", "index": 0}]] + }, + "✍️ Update tasks.json": { + "main": [[{"node": "📢 TG: Fin loop", "type": "main", "index": 0}]] + }, + "📢 TG: Fin loop": { + "main": [[{"node": "🔄 Reiniciar", "type": "main", "index": 0}]] + }, + "🔄 Reiniciar": { + "main": [[{"node": "⏰ Inicio Loop", "type": "main", "index": 0}]] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": ["pymesbot"], + "meta": {"instanceId": "pymesbot-v5-loop"} +}