286 lines
26 KiB
JSON
286 lines
26 KiB
JSON
{
|
|
"name": "PymesBot — Coding Loop (Fixed)",
|
|
"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": {
|
|
"jsCode": "const GITEA_URL = 'https://gitea.cbcren.online/';\nconst GITEA_USER = 'renato97';\nconst GITEA_TOKEN = 'efeed2af00597883adb04da70bd6a7c2993ae92d';\nconst REPO = 'pymesbot-infra';\nconst FILE_PATH = 'tasks.json';\n\nconst url = `${GITEA_URL}api/v1/repos/${GITEA_USER}/${REPO}/contents/${FILE_PATH}`;\n\nconst response = await $http.request({\n method: 'GET',\n url,\n headers: {\n 'Authorization': `token ${GITEA_TOKEN}`,\n 'Content-Type': 'application/json'\n }\n});\n\nif (!response.content) {\n throw new Error('tasks.json vacío o no encontrado en el repo');\n}\n\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": "read-tasks",
|
|
"name": "📋 Leer tasks.json de Gitea",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [220, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tasks = $input.item.json.tasks;\nconst key_pool = $input.item.json.key_pool;\nconst config = $input.item.json.config;\nconst file_sha = $input.item.json.file_sha;\nconst raw_tasks = $input.item.json.raw_tasks;\n\nconst completadas = new Set(\n tasks.filter(t => t.status === 'done').map(t => t.id)\n);\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 const fallidas = tasks.filter(t => t.intentos >= 3 && t.status !== 'done').length;\n \n return [{\n json: {\n hay_tarea: false,\n motivo: pendientes === 0 ? 'TODAS_COMPLETADAS' : 'DEPENDENCIAS_BLOQUEADAS',\n stats: {\n total: tasks.length,\n completadas: completadas.size,\n pendientes,\n fallidas\n }\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 config,\n file_sha,\n raw_tasks,\n stats: {\n total: tasks.length,\n completadas: completadas.size,\n pendientes: tasks.filter(t => t.status === 'pending').length\n }\n }\n}];"
|
|
},
|
|
"id": "select-task",
|
|
"name": "🎯 Seleccionar próxima tarea",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [440, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "check-hay-tarea",
|
|
"leftValue": "={{ $json.hay_tarea }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-hay-tarea",
|
|
"name": "¿Hay tarea disponible?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [660, 300]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const GITEA_URL = 'https://gitea.cbcren.online/';\nconst GITEA_USER = 'renato97';\nconst GITEA_TOKEN = 'efeed2af00597883adb04da70bd6a7c2993ae92d';\nconst REPO = 'pymesbot-infra';\n\nconst tarea = $input.item.json.tarea;\nconst config = $input.item.json.config;\n\nconst specFile = tarea.proyecto === 'pymesbot-installer'\n ? '02_PYMESBOT_INSTALLER_SPEC.md'\n : '01_PYMESBOT_PROJECT_SPEC.md';\n\nconst url = `${GITEA_URL}api/v1/repos/${GITEA_USER}/${REPO}/contents/specs/${specFile}`;\n\nlet specContent = '';\ntry {\n const resp = await $http.request({\n method: 'GET',\n url,\n headers: { 'Authorization': `token ${GITEA_TOKEN}` }\n });\n specContent = Buffer.from(resp.content, 'base64').toString('utf-8');\n \n if (specContent.length > 12000) {\n const seccionMencionada = tarea.contexto_spec || '';\n const matchSeccion = seccionMencionada.match(/[Ss]ecci[oó]n (\\d+)/);\n if (matchSeccion) {\n const numSeccion = matchSeccion[1];\n const headerRegex = new RegExp(`## ${numSeccion}\\.`);\n const idx = specContent.search(headerRegex);\n if (idx !== -1) {\n const start = Math.max(0, idx - 500);\n specContent = specContent.substring(start, start + 10000);\n } else {\n specContent = specContent.substring(0, 12000);\n }\n } else {\n specContent = specContent.substring(0, 12000);\n }\n }\n} catch(e) {\n specContent = `No se pudo cargar el spec: ${e.message}. Implementar según el spec del proyecto PymesBot.`;\n}\n\nreturn [{\n json: {\n ...($input.item.json),\n spec_content: specContent,\n spec_file: specFile\n }\n}];"
|
|
},
|
|
"id": "read-spec",
|
|
"name": "📖 Leer spec relevante",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [880, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tarea = $input.item.json.tarea;\nconst spec_content = $input.item.json.spec_content;\nconst key_elegida = $input.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 trabajando en el proyecto PymesBot.\nTu trabajo es implementar código de producción limpio, correcto y completo.\n\n## TAREA\nID: ${tarea.id}\nTítulo: ${tarea.titulo}\nDescripción: ${tarea.descripcion}\n\n## ARCHIVO A GENERAR\n${archivoPrincipal}\n\n## CONTEXTO DEL SPEC\n${spec_content}\n\n## REGLAS ABSOLUTAS — NUNCA VIOLARLAS\n1. Respondé ÚNICAMENTE con el código del archivo. Cero texto adicional.\n2. Sin bloques markdown, sin \\`\\`\\`python, sin explicaciones, sin comentarios fuera del código.\n3. El código empieza en la primera línea de tu respuesta y termina en la última.\n4. Seguí EXACTAMENTE los nombres de funciones, clases, rutas y formatos del spec.\n5. Todo el código debe ser Python 3.11+ válido (o HTML/JS según corresponda).\n6. Usá tipado con Pydantic/type hints donde el spec lo indica.\n7. Nunca hardcodear API keys ni contraseñas.\n8. Los comentarios en el código van en español.\n9. Si necesitás importar algo no estándar, asegurate de que esté en requirements.txt del proyecto.\n10. El archivo debe estar completo y funcional — sin TODOs ni partes incompletas.\n\nCÓDIGO:`;\n\nreturn [{\n json: {\n ...($input.item.json),\n prompt,\n archivo_objetivo: archivoPrincipal\n }\n}];"
|
|
},
|
|
"id": "build-prompt",
|
|
"name": "🔨 Construir prompt",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1100, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const GLM_KEY_1 = '6fef8efda3d24eb9ad3d718daf1ae9a1.RcFc7QPe5uZLr2mS';\nconst MINIMAX_KEY_1 = 'sk-cp-XC8cbgbVBuv1g8mMcao0ABeZu_rGEN_S22EhBUqo4lJbY_UJVqUVO5XF8hVobp8gE_39JbgQggr00TQwNdV9vP458Y_MBC_8GstvzmwhuukEGY4a2I5_L6A';\n\nconst key_elegida = $input.item.json.key_elegida;\nconst prompt = $input.item.json.prompt;\n\nconst API_CONFIGS = {\n glm: {\n base_url: 'https://api.z.ai/api/paas/v4',\n modelo: 'glm-4.7',\n api_key: GLM_KEY_1\n },\n minimax: {\n base_url: 'https://api.minimax.io/v1',\n modelo: 'MiniMax-M2.5',\n api_key: MINIMAX_KEY_1\n }\n};\n\nconst apiConfig = API_CONFIGS[key_elegida.servicio];\nif (!apiConfig || !apiConfig.api_key) {\n throw new Error(`API key no configurada para servicio: ${key_elegida.servicio}`);\n}\n\nlet codigo_generado = null;\nlet error_api = null;\nlet es_rate_limit = false;\n\ntry {\n const response = await $http.request({\n method: 'POST',\n url: `${apiConfig.base_url}/chat/completions`,\n headers: {\n 'Authorization': `Bearer ${apiConfig.api_key}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: apiConfig.modelo,\n messages: [\n {\n role: 'system',\n content: 'Sos un desarrollador senior. Respondés SOLO con código. Sin explicaciones. Sin markdown.'\n },\n {\n role: 'user',\n content: prompt\n }\n ],\n max_tokens: 4000,\n temperature: 0.1\n }),\n timeout: 90000\n });\n\n if (response.choices && response.choices[0]) {\n codigo_generado = response.choices[0].message.content;\n \n codigo_generado = codigo_generado\n .replace(/^```[a-z]*\\n?/gm, '')\n .replace(/^```\\n?/gm, '')\n .trim();\n } else {\n error_api = `Respuesta inesperada: ${JSON.stringify(response)}`;\n }\n \n} catch(e) {\n const errorStr = e.message || String(e);\n \n if (errorStr.includes('429') || errorStr.toLowerCase().includes('rate limit') || errorStr.toLowerCase().includes('too many')) {\n es_rate_limit = true;\n error_api = `Rate limit en ${key_elegida.servicio}: ${errorStr}`;\n } else {\n error_api = errorStr;\n }\n}\n\nreturn [{\n json: {\n ...($input.item.json),\n codigo_generado,\n error_api,\n es_rate_limit,\n key_usada: key_elegida.id,\n timestamp_uso: new Date().toISOString()\n }\n}];"
|
|
},
|
|
"id": "call-model",
|
|
"name": "🤖 Llamar al modelo IA",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1320, 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": [1540, 147]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const GITEA_URL = 'https://gitea.cbcren.online/';\nconst GITEA_USER = 'renato97';\nconst GITEA_TOKEN = 'efeed2af00597883adb04da70bd6a7c2993ae92d';\n\nconst codigo_generado = $input.item.json.codigo_generado;\nconst tarea = $input.item.json.tarea;\nconst archivo_objetivo = $input.item.json.archivo_objetivo;\nconst key_usada = $input.item.json.key_usada;\n\nconst REPO = tarea.proyecto;\n\nlet file_sha = null;\ntry {\n const checkUrl = `${GITEA_URL}api/v1/repos/${GITEA_USER}/${REPO}/contents/${archivo_objetivo}`;\n const existingFile = await $http.request({\n method: 'GET',\n url: checkUrl,\n headers: { 'Authorization': `token ${GITEA_TOKEN}` }\n });\n file_sha = existingFile.sha;\n} catch(e) {\n // El archivo no existe, es nuevo — OK\n}\n\nconst contenidoBase64 = Buffer.from(codigo_generado).toString('base64');\n\nconst commitMessage = `[BOT/${key_usada}] ${tarea.id} — ${tarea.titulo}`;\n\nconst body = {\n message: commitMessage,\n content: contenidoBase64,\n branch: 'main',\n author: {\n name: 'PymesBot Bot',\n email: 'bot@rvconsultas.com'\n }\n};\n\nif (file_sha) {\n body.sha = file_sha;\n}\n\nconst commitUrl = `${GITEA_URL}api/v1/repos/${GITEA_USER}/${REPO}/contents/${archivo_objetivo}`;\n\nconst commitResponse = await $http.request({\n method: file_sha ? 'PUT' : 'POST',\n url: commitUrl,\n headers: {\n 'Authorization': `token ${GITEA_TOKEN}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(body)\n});\n\nreturn [{\n json: {\n ...($input.item.json),\n commit_exitoso: true,\n commit_sha: commitResponse.commit?.sha,\n commit_message: commitMessage,\n archivo_commiteado: archivo_objetivo\n }\n}];"
|
|
},
|
|
"id": "commit-to-gitea",
|
|
"name": "📦 Commitear a Gitea",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1760, 100]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const GITEA_URL = 'https://gitea.cbcren.online/';\nconst GITEA_USER = 'renato97';\nconst GITEA_TOKEN = 'efeed2af00597883adb04da70bd6a7c2993ae92d';\nconst REPO = 'pymesbot-infra';\n\nconst input = $input.item.json;\nconst tasks_all = input.tasks_all;\nconst key_pool = input.key_pool;\nconst raw_tasks = input.raw_tasks;\nconst tarea = input.tarea;\nconst commit_exitoso = input.commit_exitoso || false;\nconst es_rate_limit = input.es_rate_limit || false;\nconst file_sha = input.file_sha;\n\nconst tasks_actualizadas = tasks_all.map(t => {\n if (t.id !== tarea.id) return t;\n \n if (commit_exitoso) {\n return {\n ...t,\n status: 'done',\n intentos: t.intentos + 1,\n modelo_usado: input.key_usada,\n completada_el: new Date().toISOString(),\n notas_error: null\n };\n } else {\n const nuevoIntentos = t.intentos + 1;\n return {\n ...t,\n status: nuevoIntentos >= 3 ? 'failed' : 'pending',\n intentos: nuevoIntentos,\n notas_error: input.error_api || 'Error desconocido'\n };\n }\n});\n\nconst key_pool_actualizado = key_pool.map(k => {\n if (k.id !== (input.key_usada || input.key_elegida?.id)) return k;\n return {\n ...k,\n ultimo_uso: new Date().toISOString(),\n usos_hoy: (k.usos_hoy || 0) + 1,\n disponible: !es_rate_limit\n };\n});\n\nconst nuevo_tasks_json = {\n ...raw_tasks,\n tareas: tasks_actualizadas,\n key_pool: key_pool_actualizado\n};\n\nconst contenidoBase64 = Buffer.from(JSON.stringify(nuevo_tasks_json, null, 2)).toString('base64');\n\nconst accion = commit_exitoso ? '✅ done' : '❌ failed';\nconst updateBody = {\n message: `[BOT] tasks.json — ${tarea.id} marcada como ${accion}`,\n content: contenidoBase64,\n sha: file_sha,\n branch: 'main',\n author: {\n name: 'PymesBot Bot',\n email: 'bot@rvconsultas.com'\n }\n};\n\nawait $http.request({\n method: 'PUT',\n url: `${GITEA_URL}api/v1/repos/${GITEA_USER}/pymesbot-infra/contents/tasks.json`,\n headers: {\n 'Authorization': `token ${GITEA_TOKEN}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(updateBody)\n});\n\nconst completadas = tasks_actualizadas.filter(t => t.status === 'done').length;\nconst pendientes = tasks_actualizadas.filter(t => t.status === 'pending').length;\nconst fallidas = tasks_actualizadas.filter(t => t.status === 'failed').length;\n\nreturn [{\n json: {\n ciclo_completado: true,\n tarea_id: tarea.id,\n resultado: commit_exitoso ? 'SUCCESS' : 'FAILED',\n archivo: input.archivo_objetivo,\n commit_sha: input.commit_sha,\n stats: { completadas, pendientes, fallidas, total: tasks_actualizadas.length }\n }\n}];"
|
|
},
|
|
"id": "update-tasks",
|
|
"name": "✍️ Actualizar tasks.json",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1980, 180]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const input = $input.item.json;\nconst es_rate_limit = input.es_rate_limit || false;\nconst error_api = input.error_api || 'Error desconocido';\nconst key_elegida = input.key_elegida;\n\nconsole.log(`[ERROR] Tarea ${input.tarea.id} falló con key ${key_elegida?.id}: ${error_api}`);\n\nif (es_rate_limit) {\n console.log(`[RATE_LIMIT] Key ${key_elegida?.id} en cooldown. Se intentará con otra key en el próximo ciclo.`);\n}\n\nreturn [{\n json: {\n ...input,\n commit_exitoso: false,\n error_registrado: error_api\n }\n}];"
|
|
},
|
|
"id": "handle-error",
|
|
"name": "⚠️ Manejar error",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1760, 280]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const input = $input.item.json;\nconsole.log(`[LOOP] Sin tareas disponibles. Motivo: ${input.motivo}`);\nconsole.log(`[LOOP] Stats: ${JSON.stringify(input.stats)}`);\n\nif (input.motivo === 'TODAS_COMPLETADAS') {\n console.log('[LOOP] 🎉 ¡PROYECTO COMPLETADO! Todas las tareas están hechas.');\n}\n\nreturn [{ json: { loop_skip: true, ...input } }];"
|
|
},
|
|
"id": "no-task-log",
|
|
"name": "💤 Sin tareas — esperar",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [880, 420]
|
|
},
|
|
{
|
|
"id": "tg-ok",
|
|
"name": "Telegram Commit OK",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [2200, 80],
|
|
"parameters": {
|
|
"jsCode": "const inp = $input.item.json;\nconst t = inp.tarea;\nconst s = inp.stats || {};\nconst tot = s.total || 30;\nconst ok = s.completadas || 0;\nconst pct = Math.round((ok/tot)*100);\nconst bar = '='.repeat(Math.round(pct/10)) + '-'.repeat(10-Math.round(pct/10));\nconst sha = (inp.commit_sha||'n/a').substring(0,8);\nconst msg = '*Commit OK* (' + t.id + ')\\n'\n + t.titulo + '\\n'\n + 'Modelo: ' + inp.key_usada + '\\n'\n + '`' + inp.archivo_objetivo + '`\\n'\n + 'sha: `' + sha + '`\\n\\n'\n + '[' + bar + '] ' + pct + '% (' + ok + '/' + tot + ')';\nawait $http.request({\n method: 'POST',\n url: 'https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n chat_id: '692714536',\n text: msg,\n parse_mode: 'Markdown',\n disable_web_page_preview: true\n })\n});\nreturn [$input.item];"
|
|
}
|
|
},
|
|
{
|
|
"id": "tg-err",
|
|
"name": "Telegram Error",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [2200, 300],
|
|
"parameters": {
|
|
"jsCode": "const inp = $input.item.json;\nconst t = inp.tarea || {};\nconst rl = inp.es_rate_limit || false;\nconst err = (inp.error_api||'desconocido').substring(0,160);\nconst intento = (t.intentos||0)+1;\nconst fatal = !rl && intento >= 3;\nconst tipo = rl ? 'Rate limit' : 'Error';\nconst fin = fatal ? 'Tarea marcada como FALLIDA.' : rl ? 'Se usara otra key.' : 'Se reintentara.';\nconst msg = tipo + ' (' + (t.id||'?') + ')\\n'\n + (t.titulo||'') + '\\n'\n + 'Key: ' + (inp.key_elegida?.id||'?') + '\\n'\n + err + '\\n'\n + 'Intento ' + intento + '/3\\n'\n + fin;\nawait $http.request({\n method: 'POST',\n url: 'https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n chat_id: '692714536',\n text: msg,\n parse_mode: 'Markdown',\n disable_web_page_preview: true\n })\n});\nreturn [$input.item];"
|
|
}
|
|
},
|
|
{
|
|
"id": "tg-idle",
|
|
"name": "Telegram Sin tareas",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [1100, 460],
|
|
"parameters": {
|
|
"jsCode": "const inp = $input.item.json;\nconst m = inp.motivo || '';\nconst s = inp.stats || {};\nlet msg = '';\nif (m === 'TODAS_COMPLETADAS') {\n msg = '*PROYECTO COMPLETADO!*\\n'\n + 'Las ' + (s.total||30) + ' tareas de PymesBot estan listas.\\n\\n'\n + 'Backend: https://gitea.cbcren.online/renato97/pymesbot-backend\\n'\n + 'Installer: https://gitea.cbcren.online/renato97/pymesbot-installer\\n\\n'\n + 'Ya podes levantar el stack en demo2.cbcren.online';\n await $http.request({\n method: 'POST',\n url: 'https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n chat_id: '692714536',\n text: msg,\n parse_mode: 'Markdown',\n disable_web_page_preview: true\n })\n});\n} else if (m === 'TODAS_KEYS_EN_COOLDOWN') {\n msg = 'Keys en cooldown. El bot retoma en 5 min.';\n await $http.request({\n method: 'POST',\n url: 'https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n chat_id: '692714536',\n text: msg,\n parse_mode: 'Markdown',\n disable_web_page_preview: true\n })\n});\n}\nreturn [$input.item];"
|
|
}
|
|
},
|
|
{
|
|
"id": "cron-hr",
|
|
"name": "Cron cada hora",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [0, 600],
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "hours",
|
|
"hoursInterval": 1
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "tg-hr",
|
|
"name": "Telegram Resumen horario",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [280, 600],
|
|
"parameters": {
|
|
"jsCode": "const GITEA_URL = 'https://gitea.cbcren.online/';\nconst GITEA_USER = 'renato97';\nconst GITEA_TOKEN = 'efeed2af00597883adb04da70bd6a7c2993ae92d';\n\nlet tareas = [];\ntry {\n const r = await $http.request({\n method:'GET',\n url: GITEA_URL+'api/v1/repos/'+GITEA_USER+'/pymesbot-infra/contents/tasks.json',\n headers:{'Authorization':'token '+GITEA_TOKEN}\n });\n tareas = JSON.parse(Buffer.from(r.content,'base64').toString()).tareas||[];\n} catch(e){return [{json:{skip:true}}];}\nconst tot = tareas.length;\nconst ok = tareas.filter(t=>t.status==='done').length;\nconst pend = tareas.filter(t=>t.status==='pending').length;\nconst fail = tareas.filter(t=>t.status==='failed').length;\nconst pct = Math.round((ok/tot)*100);\nconst bar = '='.repeat(Math.round(pct/10))+'-'.repeat(10-Math.round(pct/10));\nconst last = tareas\n .filter(t=>t.status==='done'&&t.completada_el)\n .sort((a,b)=>new Date(b.completada_el)-new Date(a.completada_el))\n .slice(0,3).map(t=>'[ok] '+t.id+' - '+t.titulo.substring(0,40)).join('\\n')||'(ninguna)';\nconst next = tareas.filter(t=>t.status==='pending')\n .slice(0,3).map(t=>'[..] '+t.id+' - '+t.titulo.substring(0,40)).join('\\n')||'(vacia)';\nconst hora = new Date().toLocaleTimeString('es-AR',{timeZone:'America/Argentina/Buenos_Aires',hour:'2-digit',minute:'2-digit'});\nconst msg = '*Resumen ' + hora + ' ART*\\n\\n'\n + '['+bar+'] '+pct+'%\\n'\n + 'OK:'+ok+' pend:'+pend+' fail:'+fail+' total:'+tot+'\\n\\n'\n + '*Ultimas:*\\n'+last+'\\n\\n'\n + '*Proximas:*\\n'+next;\nawait $http.request({\n method: 'POST',\n url: 'https://api.telegram.org/bot8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU/sendMessage',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n chat_id: '692714536',\n text: msg,\n parse_mode: 'Markdown',\n disable_web_page_preview: true\n })\n});\nreturn [{json:{pct,ok,pend,fail}}];"
|
|
}
|
|
}
|
|
],
|
|
"connections": {
|
|
"⏰ Cron cada 5 min": {
|
|
"main": [[{"node": "📋 Leer tasks.json de Gitea", "type": "main", "index": 0}]]
|
|
},
|
|
"📋 Leer tasks.json de Gitea": {
|
|
"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": "📖 Leer spec relevante", "type": "main", "index": 0}],
|
|
[{"node": "💤 Sin tareas — esperar", "type": "main", "index": 0}]
|
|
]
|
|
},
|
|
"📖 Leer spec relevante": {
|
|
"main": [[{"node": "🔨 Construir prompt", "type": "main", "index": 0}]]
|
|
},
|
|
"🔨 Construir prompt": {
|
|
"main": [[{"node": "🤖 Llamar al modelo IA", "type": "main", "index": 0}]]
|
|
},
|
|
"🤖 Llamar al modelo IA": {
|
|
"main": [[{"node": "¿Código generado?", "type": "main", "index": 0}]]
|
|
},
|
|
"¿Código generado?": {
|
|
"main": [
|
|
[{"node": "📦 Commitear a Gitea", "type": "main", "index": 0}],
|
|
[{"node": "⚠️ Manejar error", "type": "main", "index": 0}]
|
|
]
|
|
},
|
|
"📦 Commitear a Gitea": {
|
|
"main": [[{"node": "Telegram Commit OK", "type": "main", "index": 0}]]
|
|
},
|
|
"⚠️ Manejar error": {
|
|
"main": [[{"node": "Telegram Error", "type": "main", "index": 0}]]
|
|
},
|
|
"Telegram Commit OK": {
|
|
"main": [[{"node": "✍️ Actualizar tasks.json", "type": "main", "index": 0}]]
|
|
},
|
|
"Telegram Error": {
|
|
"main": [[{"node": "✍️ Actualizar tasks.json", "type": "main", "index": 0}]]
|
|
},
|
|
"💤 Sin tareas — esperar": {
|
|
"main": [[{"node": "Telegram Sin tareas", "type": "main", "index": 0}]]
|
|
},
|
|
"Cron cada hora": {
|
|
"main": [[{"node": "Telegram Resumen horario", "type": "main", "index": 0}]]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"saveManualExecutions": true,
|
|
"callerPolicy": "workflowsFromSameOwner",
|
|
"errorWorkflow": ""
|
|
},
|
|
"staticData": null,
|
|
"tags": ["pymesbot", "coding-bot", "automation"],
|
|
"meta": {
|
|
"instanceId": "pymesbot-coding-loop-v2-fixed"
|
|
}
|
|
}
|