File size: 17,696 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/**
 * ============================================
 * 🐛 debugify.js — Diagnóstico Completo Total
 * ============================================
 */

import { createWriteStream, mkdirSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import archiver from 'archiver';
import * as db from './db.js';

const LOG_BUFFER_SIZE = 1000;
const logBuffer  = [];
const errorBuffer = [];

export function initDebugify() {
  const origLog  = console.log.bind(console);
  const origErr  = console.error.bind(console);
  const origWarn = console.warn.bind(console);

  console.log = (...args) => {
    const line = `[${new Date().toISOString()}] [LOG] ${args.join(' ')}`;
    logBuffer.push(line);
    if (logBuffer.length > LOG_BUFFER_SIZE) logBuffer.shift();
    origLog(...args);
  };
  console.error = (...args) => {
    const line = `[${new Date().toISOString()}] [ERR] ${args.join(' ')}`;
    logBuffer.push(line); errorBuffer.push(line);
    if (logBuffer.length > LOG_BUFFER_SIZE) logBuffer.shift();
    if (errorBuffer.length > 500) errorBuffer.shift();
    origErr(...args);
  };
  console.warn = (...args) => {
    const line = `[${new Date().toISOString()}] [WARN] ${args.join(' ')}`;
    logBuffer.push(line);
    if (logBuffer.length > LOG_BUFFER_SIZE) logBuffer.shift();
    origWarn(...args);
  };

  process.on('uncaughtException', (err) => {
    errorBuffer.push(`[${new Date().toISOString()}] [UNCAUGHT] ${err.stack || err.message}`);
  });
  process.on('unhandledRejection', (reason) => {
    errorBuffer.push(`[${new Date().toISOString()}] [UNHANDLED] ${reason?.stack || reason}`);
  });

  console.log('[Debugify] ✅ Sistema de debug iniciado');
}

async function q(sql, args = []) {
  try {
    const r = await db.db.execute({ sql, args });
    return r.rows ?? [];
  } catch (e) {
    return [{ _error: e.message }];
  }
}

export async function generateDebugZip(client, config) {
  const now     = new Date();
  const ts      = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
  const zipName = `debugify-${ts}.zip`;
  const zipPath = join('./debugify', zipName);
  mkdirSync('./debugify', { recursive: true });

  const output  = createWriteStream(zipPath);
  const archive = archiver('zip', { zlib: { level: 9 } });

  return new Promise(async (resolve, reject) => {
    output.on('close', () => resolve({ path: zipPath, name: zipName, size: archive.pointer() }));
    archive.on('error', reject);
    archive.pipe(output);

    // 01 — LOGS
    archive.append(logBuffer.join('\n') || '(sin logs)', { name: '01-logs/console-completo.txt' });
    archive.append(errorBuffer.join('\n') || '(sin errores)', { name: '01-logs/solo-errores.txt' });
    archive.append(logBuffer.slice(-100).join('\n') || '(vacío)', { name: '01-logs/ultimas-100-lineas.txt' });

    // 02 — CONVERSACIONES USUARIO + ZELIN
    try {
      const convs = await q(`SELECT channel_id, role, content, username, created_at FROM conversation_context ORDER BY channel_id, created_at DESC LIMIT 2000`);
      const byChannel = new Map();
      for (const row of convs) {
        const ch = row.channel_id ?? 'unknown';
        if (!byChannel.has(ch)) byChannel.set(ch, []);
        const who = row.role === 'assistant' ? '🤖 Zelin' : `👤 ${row.username ?? 'Usuario'}`;
        byChannel.get(ch).push(`[${String(row.created_at ?? '').slice(0,19)}] ${who}: ${String(row.content ?? '').slice(0,500)}`);
      }
      for (const [ch, lines] of byChannel)
        archive.append(lines.reverse().join('\n\n'), { name: `02-conversaciones/canal-${ch}.txt` });
      archive.append(
        [...byChannel.entries()].map(([ch, l]) => `${ch}: ${l.length} mensajes`).join('\n') || '(sin conversaciones)',
        { name: '02-conversaciones/RESUMEN.txt' }
      );
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '02-conversaciones/error.txt' }); }

    // 03 — MENSAJES DEL SERVIDOR
    try {
      const msgs = await q(`SELECT m.channel_id, u.username, m.content, m.created_at, m.is_edited FROM messages m LEFT JOIN users u ON m.user_id = u.user_id WHERE m.is_deleted = 0 ORDER BY m.created_at DESC LIMIT 500`);
      const byChannel = new Map();
      for (const r of msgs) {
        const ch = r.channel_id ?? 'unknown';
        if (!byChannel.has(ch)) byChannel.set(ch, []);
        byChannel.get(ch).push(`[${String(r.created_at ?? '').slice(11,19)}] ${r.username ?? '?'}${r.is_edited ? '[editado]' : ''}: ${String(r.content ?? '').slice(0,200)}`);
      }
      for (const [ch, lines] of byChannel)
        archive.append(lines.reverse().join('\n'), { name: `03-mensajes-servidor/canal-${ch}.txt` });

      const deleted = await q(`SELECT username, content, channel_name, deleted_at FROM deleted_messages ORDER BY deleted_at DESC LIMIT 50`);
      archive.append(
        deleted.length ? deleted.map(r => `[${r.deleted_at}] ${r.username} en #${r.channel_name}: ${String(r.content ?? '').slice(0,150)}`).join('\n') : '(ninguno)',
        { name: '03-mensajes-servidor/mensajes-eliminados.txt' }
      );
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '03-mensajes-servidor/error.txt' }); }

    // 04 — DISCORD COMPLETO
    try {
      const guild = client?.guilds?.cache?.first();
      archive.append([
        `=== BOT ===`,
        `Tag: ${client?.user?.tag ?? '?'} | ID: ${client?.user?.id ?? '?'}`,
        `Guilds: ${client?.guilds?.cache?.size ?? 0} | Uptime: ${Math.floor((client?.uptime ?? 0)/60000)}min | Ping: ${client?.ws?.ping ?? '?'}ms`,
        ``,
        `=== GUILD ===`,
        `${guild?.name ?? 'N/A'} (ID: ${guild?.id ?? 'N/A'})`,
        `Miembros: ${guild?.memberCount ?? '?'} | Canales: ${guild?.channels?.cache?.size ?? '?'} | Roles: ${guild?.roles?.cache?.size ?? '?'}`,
        `Boost tier: ${guild?.premiumTier ?? '?'} | Boosts: ${guild?.premiumSubscriptionCount ?? 0}`,
        ``,
        `=== CANALES (${guild?.channels?.cache?.size ?? 0}) ===`,
        ...(guild?.channels?.cache?.map(ch => `  ${ch.type===2?'🔊':'#'}${ch.name} [${ch.id}]`) ?? []),
        ``,
        `=== ROLES (${guild?.roles?.cache?.size ?? 0}) ===`,
        ...(guild?.roles?.cache?.map(r => `  @${r.name}: ${r.members.size}m — ${r.id}${r.hexColor}`) ?? []),
      ].join('\n'), { name: '04-discord/estado-completo.txt' });
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '04-discord/error.txt' }); }

    // 05 — BASE DE DATOS
    try {
      const s = (await q(`SELECT (SELECT COUNT(*) FROM messages WHERE is_deleted=0) AS total_msgs, (SELECT COUNT(*) FROM messages WHERE created_at>datetime('now','-1 hour') AND is_deleted=0) AS msgs_1h, (SELECT COUNT(*) FROM messages WHERE created_at>datetime('now','-24 hours') AND is_deleted=0) AS msgs_24h, (SELECT COUNT(*) FROM messages WHERE created_at>datetime('now','-7 days') AND is_deleted=0) AS msgs_7d, (SELECT COUNT(DISTINCT user_id) FROM messages WHERE is_deleted=0) AS usuarios_con_msgs, (SELECT COUNT(*) FROM users) AS total_usuarios, (SELECT COUNT(*) FROM conversation_context) AS turns_conv, (SELECT COUNT(*) FROM zelin_memory) AS entradas_mem, (SELECT COUNT(*) FROM deleted_messages) AS msgs_eliminados`))[0] ?? {};
      archive.append([
        `=== ESTADÍSTICAS DB ===`,
        `Mensajes totales:       ${s.total_msgs}`,
        `Mensajes última hora:   ${s.msgs_1h}`,
        `Mensajes últimas 24h:   ${s.msgs_24h}`,
        `Mensajes últimos 7d:    ${s.msgs_7d}`,
        `Usuarios con mensajes:  ${s.usuarios_con_msgs}`,
        `Usuarios registrados:   ${s.total_usuarios}`,
        `Turns de conversación:  ${s.turns_conv}`,
        `Entradas de memoria:    ${s.entradas_mem}`,
        `Mensajes eliminados:    ${s.msgs_eliminados}`,
      ].join('\n'), { name: '05-database/stats.txt' });

      const topUsers = await q(`SELECT u.username, COUNT(m.id) as msgs, MAX(m.created_at) as ultimo FROM messages m JOIN users u ON m.user_id=u.user_id WHERE m.is_deleted=0 AND m.created_at>datetime('now','-7 days') GROUP BY m.user_id ORDER BY msgs DESC LIMIT 20`);
      archive.append(topUsers.map(r => `${String(r.username??'?').padEnd(20)} ${r.msgs} msgs — ${String(r.ultimo??'').slice(0,16)}`).join('\n'), { name: '05-database/top-usuarios-activos.txt' });

      const topCh = await q(`SELECT m.channel_id, COUNT(*) as msgs, MAX(m.created_at) as ultimo FROM messages m WHERE m.is_deleted=0 AND m.created_at>datetime('now','-7 days') GROUP BY m.channel_id ORDER BY msgs DESC LIMIT 20`);
      archive.append(topCh.map(r => `${String(r.channel_id).padEnd(20)} ${r.msgs} msgs — ${String(r.ultimo??'').slice(0,16)}`).join('\n'), { name: '05-database/canales-activos.txt' });

      const allUsers = await q(`SELECT username, nickname, joined_at, left_at, message_count, notes FROM users ORDER BY joined_at DESC LIMIT 200`);
      archive.append(allUsers.map(r => `${r.username??'?'} (${r.nickname??'-'}) | entró:${r.joined_at??'?'} | msgs:${r.message_count??0} | notas:${r.notes??'ninguna'}`).join('\n'), { name: '05-database/usuarios-registrados.txt' });

      const memByCat = await q(`SELECT category, COUNT(*) as n, MAX(updated_at) as ultimo FROM zelin_memory GROUP BY category ORDER BY n DESC`);
      archive.append(memByCat.map(r => `${String(r.category).padEnd(25)} ${r.n} entradas — último: ${String(r.ultimo??'').slice(0,16)}`).join('\n'), { name: '05-database/memoria-por-categoria.txt' });

      const allMem = await q(`SELECT key, value, category, updated_at, expires_at FROM zelin_memory WHERE category!='skill' ORDER BY category, updated_at DESC LIMIT 200`);
      archive.append(allMem.map(r => `[${r.category}] ${r.key}\n  ${String(r.value??'').slice(0,300)}\n  actualizado:${r.updated_at} expira:${r.expires_at??'nunca'}`).join('\n---\n'), { name: '05-database/memoria-completa.txt' });

      const events = await q(`SELECT type, user_id, target_id, old_val, new_val, created_at FROM server_events ORDER BY created_at DESC LIMIT 100`);
      archive.append(events.length ? events.map(r => `[${r.created_at}] ${r.type} user:${r.user_id} target:${r.target_id} ${r.old_val??''}${r.new_val??''}`).join('\n') : '(sin eventos)', { name: '05-database/eventos-servidor.txt' });

      const sums = await q(`SELECT channel_id, period_start, period_end, message_count, summary, top_topics FROM channel_summaries ORDER BY created_at DESC LIMIT 20`);
      archive.append(sums.length ? sums.map(r => `Canal:${r.channel_id} ${r.period_start}${r.period_end} (${r.message_count} msgs)\nTemas:${r.top_topics??'?'}\n${String(r.summary??'').slice(0,400)}`).join('\n═══\n') : '(sin resúmenes)', { name: '05-database/resumenes.txt' });

      const skills = await q(`SELECT key, value FROM zelin_memory WHERE category='skill'`);
      archive.append(skills.length ? skills.map(r => `KEY: ${r.key}\n${String(r.value??'').slice(0,500)}`).join('\n---\n') : '(sin skills)', { name: '05-database/skills.txt' });
    } catch (e) { archive.append(`Error: ${e.message}\n${e.stack??''}`, { name: '05-database/error.txt' }); }

    // 06 — ESTADO IA
    try {
      const { getLocalAIStats, isLocalAIReady, getLocalAIError } = await import('./local-ai.js');
      const { getDailyStats, getProviderStatus } = await import('./ai.js');
      const { getHealerStats } = await import('./runtime-healer.js');
      const aiStats = await getLocalAIStats().catch(() => ({}));
      const daily = getDailyStats();
      const providers = getProviderStatus();
      const healer = getHealerStats();

      archive.append([
        `=== MODELO LOCAL ===`,
        `Listo: ${isLocalAIReady()} | Error: ${getLocalAIError()??'ninguno'}`,
        `Modelo: ${aiStats.model??'?'}`,
        `Fast: ${aiStats.modelFast??'N/A'} (${aiStats.fastAvailable?'ok':'no disponible'})`,
        `Embed: ${aiStats.embedModel??'N/A'} (${aiStats.embedAvailable?'ok':'no disponible'})`,
        ``,
        `=== PROVIDERS (uso diario) ===`,
        ...Object.entries(daily).map(([n,s]) => `  ${n.padEnd(20)} req:${String(s.requests).padStart(4)}/${s.limit} (${s.pct}) state:${s.state} score:${s.score}`),
        ``,
        `=== CIRCUIT BREAKERS ===`,
        ...providers.map(p => `  ${p.name.padEnd(20)} estado:${p.state} score:${p.score}`),
        ``,
        `=== RUNTIME HEALER ===`,
        `Errores: ${healer.errors_stored} total / ${healer.errors_last_hour} última hora`,
        `Healings: ${healer.healing_this_hour}/${healer.healing_cap} | Degradado: ${healer.degraded}`,
        `Performance: ${JSON.stringify(healer.performance)}`,
      ].join('\n'), { name: '06-ia/estado-completo.txt' });

      const runtimeErrs = await q(`SELECT key, value, updated_at FROM zelin_memory WHERE category='runtime_errors' ORDER BY updated_at DESC LIMIT 50`);
      archive.append(runtimeErrs.length ? runtimeErrs.map(r => `[${r.updated_at}] ${String(r.value??'').slice(0,400)}`).join('\n---\n') : '(sin errores runtime)', { name: '06-ia/errores-runtime.txt' });

      const auditLogs = await q(`SELECT key, value, updated_at FROM zelin_memory WHERE category='audit_log' ORDER BY updated_at DESC LIMIT 30`);
      archive.append(auditLogs.length ? auditLogs.map(r => `[${r.updated_at}] ${String(r.value??'').slice(0,300)}`).join('\n---\n') : '(sin audit logs)', { name: '06-ia/audit-logs.txt' });
    } catch (e) { archive.append(`Error: ${e.message}\n${e.stack??''}`, { name: '06-ia/error.txt' }); }

    // 07 — CONFIG
    try {
      const safe = JSON.parse(JSON.stringify(config ?? {}));
      const M = '***REDACTED***';
      if (safe.discord?.token) safe.discord.token = M;
      if (safe.turso?.token) safe.turso.token = M;
      if (safe.sftp?.password) safe.sftp.password = M;
      if (safe.ai) for (const k of Object.keys(safe.ai)) {
        if (safe.ai[k]?.apiKey) safe.ai[k].apiKey = M;
        if (safe.ai[k]?.keys) safe.ai[k].keys = [`${(safe.ai[k].keys??[]).length} keys REDACTED`];
        if (safe.ai[k]?.sk) safe.ai[k].sk = M;
        if (safe.ai[k]?.apiToken) safe.ai[k].apiToken = M;
      }
      archive.append(JSON.stringify(safe, null, 2), { name: '07-config/config-sanitizada.json' });
      if (existsSync('./package.json'))
        archive.append(readFileSync('./package.json', 'utf8'), { name: '07-config/package.json' });
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '07-config/error.txt' }); }

    // 08 — SISTEMA
    const mem = process.memoryUsage();
    archive.append([
      `=== MEMORIA ===`,
      `RSS: ${(mem.rss/1024/1024).toFixed(1)}MB | Heap: ${(mem.heapUsed/1024/1024).toFixed(1)}/${(mem.heapTotal/1024/1024).toFixed(1)}MB | External: ${(mem.external/1024/1024).toFixed(1)}MB`,
      ``,
      `=== PROCESO ===`,
      `Uptime: ${Math.floor(process.uptime())}s (${(process.uptime()/3600).toFixed(2)}h)`,
      `PID: ${process.pid} | Node: ${process.version} | Platform: ${process.platform}/${process.arch}`,
      ``,
      `Generado: ${now.toISOString()}`,
    ].join('\n'), { name: '08-sistema/proceso.txt' });

    // 09 — PSYCHE
    try {
      const { getStateSnapshot } = await import('./psyche.js');
      archive.append(JSON.stringify(getStateSnapshot(), null, 2), { name: '09-psyche/estado-psicologico.json' });
      const psycheData = await q(`SELECT key, value, updated_at FROM zelin_memory WHERE category='psyche' ORDER BY updated_at DESC LIMIT 50`);
      archive.append(psycheData.length ? psycheData.map(r => `[${r.updated_at}] ${r.key}: ${String(r.value??'').slice(0,200)}`).join('\n') : '(sin datos psyche)', { name: '09-psyche/afinidades-usuarios.txt' });
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '09-psyche/error.txt' }); }

    // 10 — LEARNING
    try {
      const ld = await q(`SELECT key, value, category, updated_at FROM zelin_memory WHERE category IN ('learning','user_profile','knowledge') ORDER BY updated_at DESC LIMIT 100`);
      archive.append(ld.length ? ld.map(r => `[${r.category}] ${r.key}${r.updated_at}\n  ${String(r.value??'').slice(0,300)}`).join('\n---\n') : '(sin datos)', { name: '10-learning/datos.txt' });
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '10-learning/error.txt' }); }

    // 11 — STANDING PROMPTS
    try {
      const sp = await q(`SELECT key, value, updated_at FROM zelin_memory WHERE category='standing_prompt' OR key LIKE 'standing%' ORDER BY updated_at DESC`);
      archive.append(sp.length ? sp.map(r => `[${r.updated_at}] ${r.key}:\n${String(r.value??'').slice(0,500)}`).join('\n═══\n') : '(sin standing prompts)', { name: '11-standing-prompts/prompts.txt' });
    } catch (e) { archive.append(`Error: ${e.message}`, { name: '11-standing-prompts/error.txt' }); }

    // README
    archive.append([
      `DEBUGIFY COMPLETO — Zelin v5.8`,
      `Generado: ${now.toISOString()}`,
      ``,
      `01-logs/               → Consola completa, solo errores, últimas 100 líneas`,
      `02-conversaciones/     → Todos los chats usuario↔Zelin por canal`,
      `03-mensajes-servidor/  → Mensajes del servidor + mensajes eliminados`,
      `04-discord/            → Estado completo Discord (guild, canales, roles)`,
      `05-database/           → Stats, top usuarios, memoria, eventos, resúmenes, skills`,
      `06-ia/                 → Modelo local, providers, circuit breakers, errores runtime, audit logs`,
      `07-config/             → Config sanitizada + package.json`,
      `08-sistema/            → RAM, CPU, uptime, proceso`,
      `09-psyche/             → Estado psicológico de Zelin + afinidades con usuarios`,
      `10-learning/           → Datos de aprendizaje y perfiles de usuario`,
      `11-standing-prompts/   → Standing prompts activos en DB`,
    ].join('\n'), { name: 'README.txt' });

    archive.finalize();
  });
}