{ "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"} }