@saulwade/swl-ses 1.3.8 → 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 +108 -0
- 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 +20 -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/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 +34 -34
- 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,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;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WhatsApp Adapter — Adaptador para WhatsApp Business API / bridges.
|
|
5
|
+
*
|
|
6
|
+
* Soporta dos modos:
|
|
7
|
+
* 1. WhatsApp Business API (Cloud API de Meta) — producción
|
|
8
|
+
* 2. whatsapp-web.js (bridge no oficial) — desarrollo/personal
|
|
9
|
+
*
|
|
10
|
+
* Ambos modos son bidireccionales: envío y recepción de mensajes.
|
|
11
|
+
*
|
|
12
|
+
* Dependencia modo Business API: ninguna (HTTP nativo)
|
|
13
|
+
* Dependencia modo bridge: whatsapp-web.js (npm install whatsapp-web.js)
|
|
14
|
+
*
|
|
15
|
+
* @module gateway/adapters/whatsapp
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const BaseAdapter = require('./base');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
|
|
21
|
+
/** Límite de caracteres por mensaje de WhatsApp. */
|
|
22
|
+
const MAX_MSG_LENGTH = 4096;
|
|
23
|
+
|
|
24
|
+
/** Comandos reconocidos. */
|
|
25
|
+
const COMMANDS = {
|
|
26
|
+
'/status': 'Estado del sistema SWL',
|
|
27
|
+
'/salud': 'Ejecutar diagnóstico de salud',
|
|
28
|
+
'/sesiones': 'Listar últimas sesiones',
|
|
29
|
+
'/approve': 'Aprobar operación pendiente',
|
|
30
|
+
'/reject': 'Rechazar operación pendiente',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class WhatsAppAdapter extends BaseAdapter {
|
|
34
|
+
constructor(config) {
|
|
35
|
+
super('whatsapp', config);
|
|
36
|
+
this._client = null;
|
|
37
|
+
this._mode = config.mode || 'business'; // 'business' | 'bridge'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async start() {
|
|
41
|
+
if (this._mode === 'bridge') {
|
|
42
|
+
return this._startBridge();
|
|
43
|
+
}
|
|
44
|
+
return this._startBusinessAPI();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Modo Business API (Cloud API de Meta).
|
|
49
|
+
* Requiere: WHATSAPP_TOKEN, WHATSAPP_PHONE_ID
|
|
50
|
+
*/
|
|
51
|
+
async _startBusinessAPI() {
|
|
52
|
+
const token = this.config.token || process.env.WHATSAPP_TOKEN;
|
|
53
|
+
const phoneId = this.config.phoneId || process.env.WHATSAPP_PHONE_ID;
|
|
54
|
+
|
|
55
|
+
if (!token || !phoneId) {
|
|
56
|
+
console.log('[whatsapp] Token o Phone ID no configurado. Establecer WHATSAPP_TOKEN y WHATSAPP_PHONE_ID.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._token = token;
|
|
61
|
+
this._phoneId = phoneId;
|
|
62
|
+
this.running = true;
|
|
63
|
+
console.log('[whatsapp] Adaptador iniciado (Business API mode).');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Modo bridge con whatsapp-web.js.
|
|
68
|
+
* Requiere: npm install whatsapp-web.js
|
|
69
|
+
*/
|
|
70
|
+
async _startBridge() {
|
|
71
|
+
let wwjs;
|
|
72
|
+
try {
|
|
73
|
+
wwjs = require('whatsapp-web.js');
|
|
74
|
+
} catch (_) {
|
|
75
|
+
console.log('[whatsapp] whatsapp-web.js no instalado. Ejecutar: npm install whatsapp-web.js');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { Client, LocalAuth } = wwjs;
|
|
80
|
+
|
|
81
|
+
this._client = new Client({
|
|
82
|
+
authStrategy: new LocalAuth({ dataPath: '.planning/whatsapp-session' }),
|
|
83
|
+
puppeteer: { headless: true },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this._client.on('qr', (qr) => {
|
|
87
|
+
console.log('[whatsapp] Escanea el código QR para conectar:');
|
|
88
|
+
console.log('[whatsapp] QR:', qr.substring(0, 50) + '...');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this._client.on('ready', () => {
|
|
92
|
+
this.running = true;
|
|
93
|
+
console.log('[whatsapp] Adaptador iniciado (bridge mode).');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this._client.on('message', (msg) => {
|
|
97
|
+
if (msg.fromMe) return;
|
|
98
|
+
|
|
99
|
+
const text = msg.body || '';
|
|
100
|
+
const parts = text.split(/\s+/);
|
|
101
|
+
const command = COMMANDS[parts[0]] ? parts[0] : null;
|
|
102
|
+
const args = command ? parts.slice(1).join(' ') : '';
|
|
103
|
+
|
|
104
|
+
this._emitMessage({
|
|
105
|
+
chatId: msg.from,
|
|
106
|
+
userId: msg.from,
|
|
107
|
+
userName: msg._data?.notifyName || msg.from,
|
|
108
|
+
text: text,
|
|
109
|
+
command: command,
|
|
110
|
+
args: args,
|
|
111
|
+
raw: msg,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this._client.on('auth_failure', () => {
|
|
116
|
+
console.error('[whatsapp] Autenticación fallida. Eliminar .planning/whatsapp-session/ y reiniciar.');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await this._client.initialize();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error(`[whatsapp] Error inicializando: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async stop() {
|
|
127
|
+
if (this._client) {
|
|
128
|
+
try { await this._client.destroy(); } catch (_) {}
|
|
129
|
+
this._client = null;
|
|
130
|
+
}
|
|
131
|
+
this.running = false;
|
|
132
|
+
console.log('[whatsapp] Adaptador detenido.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async send(message) {
|
|
136
|
+
const chatId = message.chatId || this.config.defaultChat;
|
|
137
|
+
if (!chatId) return false;
|
|
138
|
+
|
|
139
|
+
const text = this.formatMessage(message);
|
|
140
|
+
|
|
141
|
+
if (this._mode === 'business') {
|
|
142
|
+
return this._sendBusinessAPI(chatId, text);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Bridge mode
|
|
146
|
+
if (!this._client) return false;
|
|
147
|
+
try {
|
|
148
|
+
await this._client.sendMessage(chatId, text);
|
|
149
|
+
return true;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[whatsapp] Error enviando: ${err.message}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Envía mensaje via WhatsApp Business Cloud API.
|
|
158
|
+
*/
|
|
159
|
+
async _sendBusinessAPI(to, text) {
|
|
160
|
+
if (!this._token || !this._phoneId) return false;
|
|
161
|
+
|
|
162
|
+
const body = JSON.stringify({
|
|
163
|
+
messaging_product: 'whatsapp',
|
|
164
|
+
to: to.replace(/[^0-9]/g, ''),
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: { body: text.substring(0, MAX_MSG_LENGTH) },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const req = https.request({
|
|
171
|
+
hostname: 'graph.facebook.com',
|
|
172
|
+
path: `/v18.0/${this._phoneId}/messages`,
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Authorization': `Bearer ${this._token}`,
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
'Content-Length': Buffer.byteLength(body),
|
|
178
|
+
},
|
|
179
|
+
timeout: 10000,
|
|
180
|
+
}, (res) => {
|
|
181
|
+
let data = '';
|
|
182
|
+
res.on('data', chunk => data += chunk);
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
req.on('error', () => resolve(false));
|
|
189
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
190
|
+
req.write(body);
|
|
191
|
+
req.end();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
formatMessage(swlMessage) {
|
|
196
|
+
const payload = swlMessage.payload || swlMessage;
|
|
197
|
+
|
|
198
|
+
if (payload.jobName) {
|
|
199
|
+
const icon = payload.status === 'completed' ? '✅' : '❌';
|
|
200
|
+
const lines = [
|
|
201
|
+
`${icon} *${payload.jobName}*`,
|
|
202
|
+
`Estado: ${payload.status}`,
|
|
203
|
+
];
|
|
204
|
+
if (payload.output) {
|
|
205
|
+
lines.push('```' + payload.output.substring(0, 800) + '```');
|
|
206
|
+
}
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return swlMessage.text || JSON.stringify(payload).substring(0, 500);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = WhatsAppAdapter;
|