feat: resolver alias y dígitos en ResponseQueue; reconciliación

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent c31ca200c6
commit 182c976697

@ -23,6 +23,9 @@ export class MaintenanceService {
this.cleanupInactiveMembersOnce().catch(err => {
console.error('❌ Error en cleanup de miembros inactivos:', err);
});
this.reconcileAliasUsersOnce().catch(err => {
console.error('❌ Error en reconcile de alias de usuarios:', err);
});
}, intervalMs);
}
@ -44,4 +47,59 @@ export class MaintenanceService {
const deleted = Number(res?.changes || 0);
return deleted;
}
/**
* Reconciliación de usuarios: fusiona IDs alias (LID u opacos) hacia el número real
* en todas las tablas relevantes, basándose en user_aliases.
* Devuelve el número de alias procesados.
*/
static async reconcileAliasUsersOnce(instance: Database = db): Promise<number> {
try {
const rows = instance.prepare(`SELECT alias, user_id FROM user_aliases WHERE alias != user_id`).all() as any[];
let merged = 0;
for (const r of rows) {
const alias = String(r.alias);
const real = String(r.user_id);
instance.transaction(() => {
const nowIso = toIsoSql(new Date());
// Asegurar existencia del usuario real
try {
instance.prepare(`INSERT OR IGNORE INTO users (id, created_at, updated_at) VALUES (?, ?, ?)`)
.run(real, nowIso, nowIso);
} catch {}
const updates = [
`UPDATE tasks SET created_by = ? WHERE created_by = ?`,
`UPDATE task_assignments SET user_id = ? WHERE user_id = ?`,
`UPDATE task_assignments SET assigned_by = ? WHERE assigned_by = ?`,
`UPDATE user_preferences SET user_id = ? WHERE user_id = ?`,
`UPDATE web_tokens SET user_id = ? WHERE user_id = ?`,
`UPDATE group_members SET user_id = ? WHERE user_id = ?`
];
for (const sql of updates) {
try {
instance.prepare(sql).run(real, alias);
} catch {
// Ignorar si la tabla no existe en este despliegue
}
}
// Intentar eliminar el usuario alias si ya no tiene referencias
try {
instance.prepare(`DELETE FROM users WHERE id = ?`).run(alias);
} catch {}
})();
merged++;
}
return merged;
} catch {
// Si no existe la tabla user_aliases o hay error de DB, no hacemos nada
return 0;
}
}
}

@ -2,6 +2,15 @@ import type { Database } from 'bun:sqlite';
import { db } from '../db';
import { IdentityService } from './identity';
import { normalizeWhatsAppId } from '../utils/whatsapp';
import { Metrics } from './metrics';
const MAX_FALLBACK_DIGITS = (() => {
const raw = (process.env.ONBOARDING_FALLBACK_MAX_DIGITS || '').trim();
const n = parseInt(raw || '15', 10);
return Number.isFinite(n) && n > 0 ? n : 15;
})();
const isDigits = (s: string) => /^\d+$/.test(s);
type QueuedResponse = {
recipient: string;
@ -117,9 +126,47 @@ export const ResponseQueue = {
const url = `${baseUrl}/message/sendText/${instance}`;
try {
// Resolver destinatario efectivo (alias → número) y validar antes de construir el payload
const rawRecipient = String(item.recipient || '');
let numberOrJid = rawRecipient;
if (rawRecipient.includes('@')) {
if (rawRecipient.endsWith('@g.us')) {
// Envío a grupo: usar el JID completo tal cual
numberOrJid = rawRecipient;
} else if (rawRecipient.endsWith('@s.whatsapp.net')) {
// JID de usuario: normalizar a dígitos
const n = normalizeWhatsAppId(rawRecipient);
if (!n || !isDigits(n)) {
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'non_numeric' }); } catch {}
return { ok: false, status: 422, error: 'unresolvable_recipient_non_numeric' };
}
if (n.length >= MAX_FALLBACK_DIGITS) {
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'too_long' }); } catch {}
return { ok: false, status: 422, error: 'unresolvable_recipient_too_long' };
}
numberOrJid = n;
} else {
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'bad_domain' }); } catch {}
return { ok: false, status: 422, error: 'unresolvable_recipient_bad_domain' };
}
} else {
// Sin dominio: resolver alias si existe y validar
const resolved = IdentityService.resolveAliasOrNull(rawRecipient) || rawRecipient;
if (!isDigits(resolved)) {
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'non_numeric' }); } catch {}
return { ok: false, status: 422, error: 'unresolvable_recipient_non_numeric' };
}
if (resolved.length >= MAX_FALLBACK_DIGITS) {
try { Metrics.inc('responses_skipped_unresolvable_recipient_total', 1, { reason: 'too_long' }); } catch {}
return { ok: false, status: 422, error: 'unresolvable_recipient_too_long' };
}
numberOrJid = resolved;
}
// Build payload, adding mentioned JIDs if present in metadata
const payload: any = {
number: item.recipient,
number: numberOrJid,
text: item.message,
};

Loading…
Cancel
Save