feat: añadir onboarding A3 en group-sync.ts y .env.example

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent 7033c6149f
commit 64096e93be

@ -20,6 +20,18 @@ WEB_BASE_URL="https://taskbot.server.brobert.net" # Host público de la web (p.
# DB_PATH="./data/tasks.db" # Si se define, ignora DATA_DIR y usa esta ruta exacta
ONBOARDING_FALLBACK_MIN_DIGITS=8 # A2: longitud mínima para conservar números en menciones/tokens; por defecto 8
# Onboarding A3 (prompts únicos por grupo)
# Habilita/deshabilita la publicación (por defecto true fuera de test)
# ONBOARDING_PROMPTS_ENABLED=true
# Permite publicación durante tests específicos
# ONBOARDING_ENABLE_IN_TEST=false
# Umbral de cobertura (publica si coverage < threshold). Por defecto 1.0
# ONBOARDING_COVERAGE_THRESHOLD=1
# Periodo de gracia tras la última verificación de miembros (segundos). Por defecto 90
# ONBOARDING_GRACE_SECONDS=90
# Cooldown entre publicaciones (días). Por defecto 7
# ONBOARDING_COOLDOWN_DAYS=7
# Sincronización de grupos (opcional)
# Intervalo en milisegundos; por defecto 86400000 (24h). En desarrollo puede bajarse (mínimo recomendable 10000ms).
# GROUP_SYNC_INTERVAL_MS=86400000

@ -663,6 +663,110 @@ export class GroupSyncService {
}
const ratio = Math.max(0, Math.min(1, total > 0 ? resolvable / total : 1));
try { Metrics.set('alias_coverage_ratio', ratio, { group_id: groupId }); } catch {}
// A3: publicación condicional del mensaje de onboarding (sin spam)
try {
// Flags y parámetros
const isTest = String(process.env.NODE_ENV || '').toLowerCase() === 'test';
const enabled =
isTest
? String(process.env.ONBOARDING_ENABLE_IN_TEST || '').toLowerCase() === 'true'
: (() => {
const v = process.env.ONBOARDING_PROMPTS_ENABLED;
return v == null ? true : ['true', '1', 'yes'].includes(String(v).toLowerCase());
})();
if (!enabled) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'disabled' }); } catch {}
return;
}
const thrRaw = Number(process.env.ONBOARDING_COVERAGE_THRESHOLD);
const threshold = Number.isFinite(thrRaw) ? Math.min(1, Math.max(0, thrRaw)) : 1;
if (ratio >= threshold) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'coverage_100' }); } catch {}
return;
}
// Gating en modo enforce: no publicar en grupos no allowed
try {
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
if (mode === 'enforce') {
try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
if (!AllowedGroups.isAllowed(groupId)) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'not_allowed' }); } catch {}
return;
}
}
} catch {}
// Grace y cooldown desde tabla groups
const rowG = this.dbInstance.prepare(`
SELECT last_verified, onboarding_prompted_at
FROM groups
WHERE id = ?
`).get(groupId) as any;
const nowMs = Date.now();
const graceRaw = Number(process.env.ONBOARDING_GRACE_SECONDS);
const graceSec = Number.isFinite(graceRaw) && graceRaw >= 0 ? Math.floor(graceRaw) : 90;
const lv = rowG?.last_verified ? String(rowG.last_verified) : null;
if (lv) {
const iso = lv.includes('T') ? lv : (lv.replace(' ', 'T') + 'Z');
const ms = Date.parse(iso);
if (Number.isFinite(ms)) {
const ageSec = Math.floor((nowMs - ms) / 1000);
if (ageSec < graceSec) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'grace_period' }); } catch {}
return;
}
}
}
const cdRaw = Number(process.env.ONBOARDING_COOLDOWN_DAYS);
const cdDays = Number.isFinite(cdRaw) && cdRaw >= 0 ? Math.floor(cdRaw) : 7;
const promptedAt = rowG?.onboarding_prompted_at ? String(rowG.onboarding_prompted_at) : null;
if (promptedAt) {
const iso = promptedAt.includes('T') ? promptedAt : (promptedAt.replace(' ', 'T') + 'Z');
const ms = Date.parse(iso);
if (Number.isFinite(ms)) {
const diffMs = nowMs - ms;
if (diffMs < cdDays * 24 * 60 * 60 * 1000) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'cooldown_active' }); } catch {}
return;
}
}
}
// Número del bot para construir wa.me
const bot = String(process.env.CHATBOT_PHONE_NUMBER || '').trim();
if (!bot || !/^\d+$/.test(bot)) {
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'missing_bot_number' }); } catch {}
return;
}
// Encolar mensaje en la cola persistente y marcar timestamp en groups
const msg = `Para poder asignarte tareas y acceder a la web, envía 'hola' al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/${bot}`;
this.dbInstance.transaction(() => {
this.dbInstance.prepare(`
INSERT INTO response_queue (recipient, message, status, attempts, metadata, created_at, updated_at, next_attempt_at)
VALUES (?, ?, 'queued', 0, NULL, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
`).run(groupId, msg);
this.dbInstance.prepare(`
UPDATE groups
SET onboarding_prompted_at = strftime('%Y-%m-%d %H:%M:%f','now')
WHERE id = ?
`).run(groupId);
})();
try { Metrics.inc('onboarding_prompts_sent_total', 1, { group_id: groupId, reason: 'coverage_below_threshold' }); } catch {}
} catch (e) {
// Evitar romper el flujo si falla el encolado
if (process.env.NODE_ENV !== 'test') {
console.warn('⚠️ Onboarding prompt skipped due to internal error for', groupId, e);
}
}
} catch (e) {
console.warn('⚠️ No se pudo calcular alias_coverage_ratio para', groupId, e);
}

Loading…
Cancel
Save