@saulwade/swl-ses 1.3.7 → 1.4.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/CLAUDE.md +12 -4
- package/README.md +1 -1
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-webhook-server.js +198 -0
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +21 -1
- package/comandos/swl/claudemd.md +14 -1
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/exportar-vault.md +207 -7
- package/comandos/swl/nuevo-proyecto.md +24 -2
- package/gateway/adapters/base.js +109 -0
- package/gateway/adapters/discord.js +167 -0
- package/gateway/adapters/email.js +221 -0
- package/gateway/adapters/slack.js +192 -0
- package/gateway/adapters/telegram.js +183 -0
- package/gateway/adapters/webhook.js +113 -0
- package/gateway/adapters/whatsapp.js +214 -0
- package/gateway/agent-executor.js +322 -0
- package/gateway/command-relay.js +271 -0
- package/gateway/cron/jobs.js +263 -0
- package/gateway/cron/scheduler.js +322 -0
- package/gateway/cron/store.js +335 -0
- package/gateway/index.js +320 -0
- package/gateway/lib/event-channel.js +191 -0
- package/gateway/session.js +131 -0
- package/gateway/webhook-server.js +324 -0
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/build-errors-nextjs/SKILL.md +55 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nextjs-testing/SKILL.md +89 -5
- package/habilidades/node-experto/SKILL.md +37 -1
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/react-experto/SKILL.md +45 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/swl-claudemd/SKILL.md +15 -1
- package/habilidades/tdd-workflow/SKILL.md +36 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/inyeccion-contexto.js +8 -3
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-ip.js +177 -0
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/webhook-dedup.js +184 -0
- package/hooks/lib/webhook-verify.js +123 -0
- package/hooks/proteccion-rutas.js +120 -15
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1 -0
- package/manifiestos/skills-lock.json +37 -37
- package/package.json +5 -3
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/reglas/usar-sistema-swl.md +251 -0
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/comandos/skills.js +251 -2
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-release.js +110 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Email Adapter — Adaptador para notificaciones por correo electrónico.
|
|
5
|
+
*
|
|
6
|
+
* Soporta:
|
|
7
|
+
* - Envío via SMTP directo (nodemailer)
|
|
8
|
+
* - Envío via API de servicios (SendGrid, Mailgun, SES) — HTTP nativo
|
|
9
|
+
* - Formato HTML con template básico para notificaciones
|
|
10
|
+
* - Unidireccional (solo envío, sin recepción)
|
|
11
|
+
*
|
|
12
|
+
* Dependencia SMTP: nodemailer (npm install nodemailer)
|
|
13
|
+
* Dependencia API: ninguna (HTTP nativo)
|
|
14
|
+
*
|
|
15
|
+
* @module gateway/adapters/email
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const BaseAdapter = require('./base');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
|
|
21
|
+
class EmailAdapter extends BaseAdapter {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
super('email', config);
|
|
24
|
+
this._transporter = null;
|
|
25
|
+
this._mode = config.mode || 'api'; // 'smtp' | 'api'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async start() {
|
|
29
|
+
if (this._mode === 'smtp') {
|
|
30
|
+
return this._startSMTP();
|
|
31
|
+
}
|
|
32
|
+
return this._startAPI();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Modo SMTP con nodemailer.
|
|
37
|
+
*/
|
|
38
|
+
async _startSMTP() {
|
|
39
|
+
let nodemailer;
|
|
40
|
+
try {
|
|
41
|
+
nodemailer = require('nodemailer');
|
|
42
|
+
} catch (_) {
|
|
43
|
+
console.log('[email] nodemailer no instalado. Ejecutar: npm install nodemailer');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const host = this.config.smtpHost || process.env.SMTP_HOST;
|
|
48
|
+
const port = this.config.smtpPort || process.env.SMTP_PORT || 587;
|
|
49
|
+
const user = this.config.smtpUser || process.env.SMTP_USER;
|
|
50
|
+
const pass = this.config.smtpPass || process.env.SMTP_PASS;
|
|
51
|
+
|
|
52
|
+
if (!host || !user) {
|
|
53
|
+
console.log('[email] SMTP no configurado. Establecer SMTP_HOST y SMTP_USER.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._transporter = nodemailer.createTransport({
|
|
58
|
+
host,
|
|
59
|
+
port: Number(port),
|
|
60
|
+
secure: Number(port) === 465,
|
|
61
|
+
auth: { user, pass },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.running = true;
|
|
65
|
+
console.log(`[email] Adaptador iniciado (SMTP: ${host}:${port}).`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Modo API (SendGrid, Mailgun, SES — via HTTP).
|
|
70
|
+
*/
|
|
71
|
+
async _startAPI() {
|
|
72
|
+
const apiKey = this.config.apiKey || process.env.EMAIL_API_KEY;
|
|
73
|
+
const service = this.config.service || process.env.EMAIL_SERVICE || 'sendgrid';
|
|
74
|
+
|
|
75
|
+
if (!apiKey) {
|
|
76
|
+
console.log(`[email] API key no configurada. Establecer EMAIL_API_KEY para ${service}.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this._apiKey = apiKey;
|
|
81
|
+
this._service = service;
|
|
82
|
+
this.running = true;
|
|
83
|
+
console.log(`[email] Adaptador iniciado (API: ${service}).`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async stop() {
|
|
87
|
+
if (this._transporter) {
|
|
88
|
+
this._transporter.close();
|
|
89
|
+
this._transporter = null;
|
|
90
|
+
}
|
|
91
|
+
this.running = false;
|
|
92
|
+
console.log('[email] Adaptador detenido.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async send(message) {
|
|
96
|
+
const to = message.chatId || this.config.defaultTo || process.env.EMAIL_DEFAULT_TO;
|
|
97
|
+
const from = this.config.from || process.env.EMAIL_FROM || 'swl-ses@noreply.com';
|
|
98
|
+
|
|
99
|
+
if (!to) return false;
|
|
100
|
+
|
|
101
|
+
const subject = this._buildSubject(message);
|
|
102
|
+
const html = this._buildHTML(message);
|
|
103
|
+
const text = this.formatMessage(message);
|
|
104
|
+
|
|
105
|
+
if (this._mode === 'smtp') {
|
|
106
|
+
return this._sendSMTP(from, to, subject, text, html);
|
|
107
|
+
}
|
|
108
|
+
return this._sendAPI(from, to, subject, text, html);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Envía via SMTP con nodemailer.
|
|
113
|
+
*/
|
|
114
|
+
async _sendSMTP(from, to, subject, text, html) {
|
|
115
|
+
if (!this._transporter) return false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await this._transporter.sendMail({ from, to, subject, text, html });
|
|
119
|
+
return true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`[email] Error SMTP: ${err.message}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Envía via SendGrid API (HTTP POST).
|
|
128
|
+
*/
|
|
129
|
+
async _sendAPI(from, to, subject, text, html) {
|
|
130
|
+
if (!this._apiKey) return false;
|
|
131
|
+
|
|
132
|
+
if (this._service === 'sendgrid') {
|
|
133
|
+
return this._sendSendGrid(from, to, subject, text, html);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fallback genérico — tratar como webhook
|
|
137
|
+
console.log(`[email] Servicio "${this._service}" no implementado. Usar "sendgrid" o modo "smtp".`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* SendGrid v3 API.
|
|
143
|
+
*/
|
|
144
|
+
_sendSendGrid(from, to, subject, text, html) {
|
|
145
|
+
const body = JSON.stringify({
|
|
146
|
+
personalizations: [{ to: [{ email: to }] }],
|
|
147
|
+
from: { email: from },
|
|
148
|
+
subject,
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text/plain', value: text },
|
|
151
|
+
{ type: 'text/html', value: html },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const req = https.request({
|
|
157
|
+
hostname: 'api.sendgrid.com',
|
|
158
|
+
path: '/v3/mail/send',
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Authorization': `Bearer ${this._apiKey}`,
|
|
162
|
+
'Content-Type': 'application/json',
|
|
163
|
+
'Content-Length': Buffer.byteLength(body),
|
|
164
|
+
},
|
|
165
|
+
timeout: 10000,
|
|
166
|
+
}, (res) => {
|
|
167
|
+
let data = '';
|
|
168
|
+
res.on('data', chunk => data += chunk);
|
|
169
|
+
res.on('end', () => resolve(res.statusCode >= 200 && res.statusCode < 300));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
req.on('error', () => resolve(false));
|
|
173
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
174
|
+
req.write(body);
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_buildSubject(message) {
|
|
180
|
+
const payload = message.payload || message;
|
|
181
|
+
const status = payload.status === 'completed' ? '✓' : '✗';
|
|
182
|
+
return `[SWL] ${status} ${payload.jobName || payload.title || 'Notificación'}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_buildHTML(message) {
|
|
186
|
+
const payload = message.payload || message;
|
|
187
|
+
const isError = payload.status === 'error';
|
|
188
|
+
const color = isError ? '#dc3545' : '#28a745';
|
|
189
|
+
|
|
190
|
+
return `
|
|
191
|
+
<!DOCTYPE html>
|
|
192
|
+
<html><body style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
|
193
|
+
<div style="border-left:4px solid ${color};padding:12px 16px;margin-bottom:16px">
|
|
194
|
+
<h2 style="margin:0 0 8px;color:${color}">${payload.jobName || 'SWL Notificación'}</h2>
|
|
195
|
+
<p style="margin:0;color:#666">Estado: <strong>${payload.status || 'unknown'}</strong></p>
|
|
196
|
+
</div>
|
|
197
|
+
${payload.output ? `<pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto;font-size:13px">${_escapeHTML(payload.output.substring(0, 2000))}</pre>` : ''}
|
|
198
|
+
<hr style="border:none;border-top:1px solid #eee;margin:20px 0">
|
|
199
|
+
<p style="color:#999;font-size:12px">Enviado por SWL-SES Gateway — ${new Date().toISOString()}</p>
|
|
200
|
+
</body></html>`.trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
formatMessage(swlMessage) {
|
|
204
|
+
const payload = swlMessage.payload || swlMessage;
|
|
205
|
+
|
|
206
|
+
if (payload.jobName) {
|
|
207
|
+
const icon = payload.status === 'completed' ? '[OK]' : '[ERROR]';
|
|
208
|
+
const lines = [`${icon} ${payload.jobName}`, `Estado: ${payload.status}`];
|
|
209
|
+
if (payload.output) lines.push('', payload.output.substring(0, 1000));
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return swlMessage.text || JSON.stringify(payload).substring(0, 500);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _escapeHTML(str) {
|
|
218
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = EmailAdapter;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slack Adapter — Adaptador para Slack via Bolt SDK.
|
|
5
|
+
*
|
|
6
|
+
* Soporta:
|
|
7
|
+
* - Envío de notificaciones con Blocks (formato rico)
|
|
8
|
+
* - Recepción de comandos slash y mensajes directos
|
|
9
|
+
* - Threads para conversaciones agrupadas
|
|
10
|
+
* - Rate limiting (Slack: 1 msg/seg por canal)
|
|
11
|
+
*
|
|
12
|
+
* Dependencia: @slack/bolt (npm install @slack/bolt)
|
|
13
|
+
* Si no está instalada, el adaptador se desactiva silenciosamente.
|
|
14
|
+
*
|
|
15
|
+
* @module gateway/adapters/slack
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const BaseAdapter = require('./base');
|
|
19
|
+
|
|
20
|
+
/** Límite de caracteres por mensaje de Slack. */
|
|
21
|
+
const MAX_MSG_LENGTH = 3000;
|
|
22
|
+
|
|
23
|
+
/** Comandos reconocidos en mensajes directos al bot. */
|
|
24
|
+
const COMMANDS = {
|
|
25
|
+
'status': 'Estado del sistema SWL',
|
|
26
|
+
'salud': 'Ejecutar diagnóstico de salud',
|
|
27
|
+
'sesiones': 'Listar últimas sesiones',
|
|
28
|
+
'approve': 'Aprobar operación pendiente',
|
|
29
|
+
'reject': 'Rechazar operación pendiente',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class SlackAdapter extends BaseAdapter {
|
|
33
|
+
constructor(config) {
|
|
34
|
+
super('slack', config);
|
|
35
|
+
this._app = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start() {
|
|
39
|
+
let Bolt;
|
|
40
|
+
try {
|
|
41
|
+
Bolt = require('@slack/bolt');
|
|
42
|
+
} catch (_) {
|
|
43
|
+
console.log('[slack] @slack/bolt no instalado. Ejecutar: npm install @slack/bolt');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const token = this.config.token || process.env.SLACK_BOT_TOKEN;
|
|
48
|
+
const sigSecret = this.config.signingSecret || process.env.SLACK_SIGNING_SECRET;
|
|
49
|
+
const appToken = this.config.appToken || process.env.SLACK_APP_TOKEN;
|
|
50
|
+
|
|
51
|
+
if (!token) {
|
|
52
|
+
console.log('[slack] Token no configurado. Establecer SLACK_BOT_TOKEN.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const opts = {
|
|
58
|
+
token,
|
|
59
|
+
signingSecret: sigSecret || 'dummy',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Socket mode (recomendado, sin servidor público)
|
|
63
|
+
if (appToken) {
|
|
64
|
+
opts.socketMode = true;
|
|
65
|
+
opts.appToken = appToken;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._app = new Bolt.App(opts);
|
|
69
|
+
|
|
70
|
+
// Escuchar mensajes directos al bot
|
|
71
|
+
this._app.message(async ({ message, say }) => {
|
|
72
|
+
if (message.subtype) return; // Ignorar ediciones, joins, etc.
|
|
73
|
+
|
|
74
|
+
const text = message.text || '';
|
|
75
|
+
const parts = text.trim().split(/\s+/);
|
|
76
|
+
const cmdWord = parts[0].toLowerCase().replace(/^\//, '');
|
|
77
|
+
const command = COMMANDS[cmdWord] ? cmdWord : null;
|
|
78
|
+
const args = command ? parts.slice(1).join(' ') : '';
|
|
79
|
+
|
|
80
|
+
this._emitMessage({
|
|
81
|
+
chatId: message.channel,
|
|
82
|
+
userId: message.user,
|
|
83
|
+
userName: message.user,
|
|
84
|
+
text: text,
|
|
85
|
+
command: command ? `/${command}` : null,
|
|
86
|
+
args: args,
|
|
87
|
+
threadTs: message.thread_ts || message.ts,
|
|
88
|
+
raw: message,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (appToken) {
|
|
93
|
+
await this._app.start();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.running = true;
|
|
97
|
+
console.log(`[slack] Adaptador iniciado (${appToken ? 'socket' : 'events'} mode).`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`[slack] Error al iniciar: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async stop() {
|
|
104
|
+
if (this._app) {
|
|
105
|
+
try { await this._app.stop(); } catch (_) {}
|
|
106
|
+
this._app = null;
|
|
107
|
+
}
|
|
108
|
+
this.running = false;
|
|
109
|
+
console.log('[slack] Adaptador detenido.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async send(message) {
|
|
113
|
+
if (!this._app) return false;
|
|
114
|
+
|
|
115
|
+
const channel = message.chatId || this.config.defaultChannel || process.env.SLACK_DEFAULT_CHANNEL;
|
|
116
|
+
if (!channel) return false;
|
|
117
|
+
|
|
118
|
+
const text = this.formatMessage(message);
|
|
119
|
+
const blocks = this._buildBlocks(message);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await this._app.client.chat.postMessage({
|
|
123
|
+
token: this.config.token || process.env.SLACK_BOT_TOKEN,
|
|
124
|
+
channel,
|
|
125
|
+
text,
|
|
126
|
+
blocks,
|
|
127
|
+
unfurl_links: false,
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`[slack] Error enviando: ${err.message}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Construye Slack Blocks para notificaciones estructuradas.
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
_buildBlocks(message) {
|
|
141
|
+
const payload = message.payload || message;
|
|
142
|
+
if (!payload.jobName && !payload.title) return undefined;
|
|
143
|
+
|
|
144
|
+
const isError = payload.status === 'error';
|
|
145
|
+
const icon = isError ? ':x:' : ':white_check_mark:';
|
|
146
|
+
const blocks = [];
|
|
147
|
+
|
|
148
|
+
blocks.push({
|
|
149
|
+
type: 'header',
|
|
150
|
+
text: { type: 'plain_text', text: `${payload.jobName || payload.title || 'SWL'}` },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
blocks.push({
|
|
154
|
+
type: 'section',
|
|
155
|
+
fields: [
|
|
156
|
+
{ type: 'mrkdwn', text: `*Estado:*\n${icon} \`${payload.status || 'unknown'}\`` },
|
|
157
|
+
{ type: 'mrkdwn', text: `*Fuente:*\nSWL-SES Gateway` },
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (payload.output) {
|
|
162
|
+
blocks.push({
|
|
163
|
+
type: 'section',
|
|
164
|
+
text: { type: 'mrkdwn', text: '```' + payload.output.substring(0, 2500) + '```' },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
blocks.push({ type: 'divider' });
|
|
169
|
+
|
|
170
|
+
return blocks;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
formatMessage(swlMessage) {
|
|
174
|
+
const payload = swlMessage.payload || swlMessage;
|
|
175
|
+
|
|
176
|
+
if (payload.jobName) {
|
|
177
|
+
const icon = payload.status === 'completed' ? ':white_check_mark:' : ':x:';
|
|
178
|
+
const lines = [
|
|
179
|
+
`${icon} *${payload.jobName}*`,
|
|
180
|
+
`Estado: \`${payload.status}\``,
|
|
181
|
+
];
|
|
182
|
+
if (payload.output) {
|
|
183
|
+
lines.push('```' + payload.output.substring(0, 1000) + '```');
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return swlMessage.text || JSON.stringify(payload).substring(0, 500);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = SlackAdapter;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Adapter — Adaptador de plataforma para Telegram Bot API.
|
|
5
|
+
*
|
|
6
|
+
* Usa polling HTTP (sin webhook, sin servidor público). Soporta:
|
|
7
|
+
* - Recepción de mensajes y comandos (/status, /salud, /approve)
|
|
8
|
+
* - Envío de notificaciones con formato Markdown
|
|
9
|
+
* - Batching de mensajes largos (split a 4096 chars)
|
|
10
|
+
* - Rate limiting (30 msg/seg Telegram limit)
|
|
11
|
+
* - Filtro de usuarios permitidos (allowedUsers)
|
|
12
|
+
*
|
|
13
|
+
* Dependencia: node-telegram-bot-api (npm install node-telegram-bot-api)
|
|
14
|
+
* Si no está instalada, el adaptador se desactiva silenciosamente.
|
|
15
|
+
*
|
|
16
|
+
* Inspirado en Hermes Agent (gateway/platforms/telegram.py).
|
|
17
|
+
*
|
|
18
|
+
* @module gateway/adapters/telegram
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const BaseAdapter = require('./base');
|
|
22
|
+
|
|
23
|
+
/** Límite de caracteres por mensaje de Telegram. */
|
|
24
|
+
const MAX_MSG_LENGTH = 4096;
|
|
25
|
+
|
|
26
|
+
/** Comandos reconocidos por el bot. */
|
|
27
|
+
const COMMANDS = {
|
|
28
|
+
'/status': 'Estado del sistema SWL',
|
|
29
|
+
'/salud': 'Ejecutar diagnóstico de salud',
|
|
30
|
+
'/sesiones': 'Listar últimas sesiones',
|
|
31
|
+
'/approve': 'Aprobar operación pendiente',
|
|
32
|
+
'/reject': 'Rechazar operación pendiente',
|
|
33
|
+
'/help': 'Mostrar ayuda',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class TelegramAdapter extends BaseAdapter {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
super('telegram', config);
|
|
39
|
+
this._bot = null;
|
|
40
|
+
this._allowedUsers = new Set(config.allowedUsers || []);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start() {
|
|
44
|
+
let TelegramBot;
|
|
45
|
+
try {
|
|
46
|
+
TelegramBot = require('node-telegram-bot-api');
|
|
47
|
+
} catch (_) {
|
|
48
|
+
console.log('[telegram] node-telegram-bot-api no instalado. Ejecutar: npm install node-telegram-bot-api');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const token = this.config.token || process.env.TELEGRAM_BOT_TOKEN;
|
|
53
|
+
if (!token) {
|
|
54
|
+
console.log('[telegram] Token no configurado. Establecer TELEGRAM_BOT_TOKEN.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._bot = new TelegramBot(token, { polling: true });
|
|
59
|
+
this.running = true;
|
|
60
|
+
|
|
61
|
+
this._bot.on('message', (msg) => {
|
|
62
|
+
// Filtrar usuarios no permitidos
|
|
63
|
+
if (this._allowedUsers.size > 0 && !this._allowedUsers.has(String(msg.from.id))) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const text = msg.text || '';
|
|
68
|
+
|
|
69
|
+
// Parsear comando
|
|
70
|
+
const parts = text.split(/\s+/);
|
|
71
|
+
const command = parts[0];
|
|
72
|
+
const args = parts.slice(1).join(' ');
|
|
73
|
+
|
|
74
|
+
this._emitMessage({
|
|
75
|
+
chatId: String(msg.chat.id),
|
|
76
|
+
userId: String(msg.from.id),
|
|
77
|
+
userName: msg.from.username || msg.from.first_name || 'unknown',
|
|
78
|
+
text: text,
|
|
79
|
+
command: COMMANDS[command] ? command : null,
|
|
80
|
+
args: args,
|
|
81
|
+
raw: msg,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this._bot.on('polling_error', (err) => {
|
|
86
|
+
// Log silencioso — no crashear
|
|
87
|
+
if (err.code === 'ETELEGRAM' && err.message.includes('409')) {
|
|
88
|
+
console.error('[telegram] Conflicto: otro bot con el mismo token. Deteniendo polling.');
|
|
89
|
+
this.stop();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log('[telegram] Adaptador iniciado (polling mode).');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stop() {
|
|
97
|
+
if (this._bot) {
|
|
98
|
+
try { await this._bot.stopPolling(); } catch (_) {}
|
|
99
|
+
this._bot = null;
|
|
100
|
+
}
|
|
101
|
+
this.running = false;
|
|
102
|
+
console.log('[telegram] Adaptador detenido.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async send(message) {
|
|
106
|
+
if (!this._bot) return false;
|
|
107
|
+
|
|
108
|
+
const chatId = message.chatId || this.config.homeChat;
|
|
109
|
+
if (!chatId) return false;
|
|
110
|
+
|
|
111
|
+
const text = this.formatMessage(message);
|
|
112
|
+
|
|
113
|
+
// Batching: split en chunks de MAX_MSG_LENGTH
|
|
114
|
+
const chunks = _splitMessage(text, MAX_MSG_LENGTH);
|
|
115
|
+
|
|
116
|
+
for (const chunk of chunks) {
|
|
117
|
+
try {
|
|
118
|
+
await this._bot.sendMessage(chatId, chunk, {
|
|
119
|
+
parse_mode: 'Markdown',
|
|
120
|
+
disable_web_page_preview: true,
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Retry sin Markdown si falla el parse
|
|
124
|
+
try {
|
|
125
|
+
await this._bot.sendMessage(chatId, chunk);
|
|
126
|
+
} catch (_) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
formatMessage(swlMessage) {
|
|
136
|
+
const payload = swlMessage.payload || swlMessage;
|
|
137
|
+
|
|
138
|
+
if (payload.jobName) {
|
|
139
|
+
// Notificación de cron
|
|
140
|
+
const status = payload.status === 'completed' ? '✓' : '✗';
|
|
141
|
+
const lines = [
|
|
142
|
+
`${status} *${payload.jobName}*`,
|
|
143
|
+
`Estado: \`${payload.status}\``,
|
|
144
|
+
];
|
|
145
|
+
if (payload.output) {
|
|
146
|
+
lines.push('```', payload.output.substring(0, 1000), '```');
|
|
147
|
+
}
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Mensaje genérico
|
|
152
|
+
return swlMessage.text || JSON.stringify(payload).substring(0, 500);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Divide un mensaje largo en chunks respetando el límite de caracteres.
|
|
158
|
+
* Intenta dividir en líneas completas.
|
|
159
|
+
*/
|
|
160
|
+
function _splitMessage(text, maxLength) {
|
|
161
|
+
if (text.length <= maxLength) return [text];
|
|
162
|
+
|
|
163
|
+
const chunks = [];
|
|
164
|
+
let remaining = text;
|
|
165
|
+
|
|
166
|
+
while (remaining.length > 0) {
|
|
167
|
+
if (remaining.length <= maxLength) {
|
|
168
|
+
chunks.push(remaining);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Buscar el último newline dentro del límite
|
|
173
|
+
let splitAt = remaining.lastIndexOf('\n', maxLength);
|
|
174
|
+
if (splitAt < maxLength * 0.3) splitAt = maxLength; // Si no hay newline útil, cortar
|
|
175
|
+
|
|
176
|
+
chunks.push(remaining.substring(0, splitAt));
|
|
177
|
+
remaining = remaining.substring(splitAt).trimStart();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return chunks;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = TelegramAdapter;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook Adapter — Adaptador genérico para webhooks HTTP.
|
|
5
|
+
*
|
|
6
|
+
* Envía notificaciones a un endpoint configurable via POST.
|
|
7
|
+
* No recibe mensajes (unidireccional: SWL → Webhook).
|
|
8
|
+
*
|
|
9
|
+
* Compatible con: Slack Incoming Webhooks, Microsoft Teams, ntfy.sh,
|
|
10
|
+
* Pushover, cualquier endpoint que acepte JSON POST.
|
|
11
|
+
*
|
|
12
|
+
* @module gateway/adapters/webhook
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const BaseAdapter = require('./base');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const { URL } = require('url');
|
|
19
|
+
|
|
20
|
+
class WebhookAdapter extends BaseAdapter {
|
|
21
|
+
constructor(config) {
|
|
22
|
+
super('webhook', config);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start() {
|
|
26
|
+
const url = this.config.url || process.env.WEBHOOK_URL;
|
|
27
|
+
if (!url) {
|
|
28
|
+
console.log('[webhook] URL no configurada. Establecer WEBHOOK_URL.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.running = true;
|
|
33
|
+
console.log('[webhook] Adaptador iniciado (unidireccional).');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async stop() {
|
|
37
|
+
this.running = false;
|
|
38
|
+
console.log('[webhook] Adaptador detenido.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async send(message) {
|
|
42
|
+
const url = this.config.url || process.env.WEBHOOK_URL;
|
|
43
|
+
if (!url) return false;
|
|
44
|
+
|
|
45
|
+
const payload = {
|
|
46
|
+
source: 'swl-ses',
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
...message,
|
|
49
|
+
text: this.formatMessage(message),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await _postJSON(url, payload, this.config.secret || process.env.WEBHOOK_SECRET);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`[webhook] Error: ${err.message}`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* POST JSON a una URL con timeout y HMAC opcional.
|
|
64
|
+
* @param {string} url
|
|
65
|
+
* @param {object} data
|
|
66
|
+
* @param {string} [secret]
|
|
67
|
+
* @returns {Promise<string>}
|
|
68
|
+
*/
|
|
69
|
+
function _postJSON(url, data, secret) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const parsed = new URL(url);
|
|
72
|
+
const client = parsed.protocol === 'https:' ? https : http;
|
|
73
|
+
const body = JSON.stringify(data);
|
|
74
|
+
const headers = {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'Content-Length': Buffer.byteLength(body),
|
|
77
|
+
'User-Agent': 'SWL-SES-Gateway/1.0',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// HMAC signature si hay secret
|
|
81
|
+
if (secret) {
|
|
82
|
+
const crypto = require('crypto');
|
|
83
|
+
const sig = crypto.createHmac('sha256', secret).update(body).digest('hex');
|
|
84
|
+
headers['X-SWL-Signature'] = `sha256=${sig}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const req = client.request({
|
|
88
|
+
hostname: parsed.hostname,
|
|
89
|
+
port: parsed.port,
|
|
90
|
+
path: parsed.pathname + parsed.search,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers,
|
|
93
|
+
timeout: 10000,
|
|
94
|
+
}, (res) => {
|
|
95
|
+
let responseBody = '';
|
|
96
|
+
res.on('data', chunk => responseBody += chunk);
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
99
|
+
resolve(responseBody);
|
|
100
|
+
} else {
|
|
101
|
+
reject(new Error(`HTTP ${res.statusCode}: ${responseBody.substring(0, 200)}`));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
req.on('error', reject);
|
|
107
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
108
|
+
req.write(body);
|
|
109
|
+
req.end();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = WebhookAdapter;
|