feat: añadir router de comandos (Etapa 1) y shared.ts; usar route

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 4 days ago
parent d40e5e7990
commit 170859c030

@ -12,6 +12,7 @@ import { AllowedGroups } from './allowed-groups';
import { Metrics } from './metrics';
import { ResponseQueue } from './response-queue';
import { randomTokenBase64Url, sha256Hex } from '../utils/crypto';
import { route as routeCommand } from './commands';
type CommandContext = {
sender: string; // normalized user id (digits only), but accept raw too
@ -1442,7 +1443,8 @@ Puedes interactuar escribiéndome por privado:
}
try {
const responses = await this.processTareaCommand(context);
const routed = await routeCommand(context);
const responses = routed ?? (await this.processTareaCommand(context));
// Clasificación explícita del outcome (evita lógica en server)
const tokens = msg.split(/\s+/);

@ -0,0 +1,26 @@
/**
* Router de comandos (Etapa 1)
* Por ahora no maneja nada y devuelve null para forzar fallback al CommandService actual.
* Nota: No importar CommandService aquí para evitar ciclos de import.
*/
export type RoutedMessage = {
recipient: string;
message: string;
mentions?: string[];
};
export type RouteContext = {
sender: string;
groupId: string;
message: string;
mentions: string[];
messageId?: string;
participant?: string;
fromMe?: boolean;
};
export async function route(_context: RouteContext): Promise<RoutedMessage[] | null> {
// En esta etapa no se maneja nada; devolver null para usar el código actual.
return null;
}

@ -0,0 +1,123 @@
/**
* Utilidades compartidas para handlers de comandos (Etapa 1).
* Aún no se usan desde CommandService; servirán en etapas siguientes.
*/
import { TaskService } from '../../tasks/service';
import { GroupSyncService } from '../group-sync';
export const ACTION_ALIASES: Record<string, string> = {
'n': 'nueva',
'nueva': 'nueva',
'crear': 'nueva',
'+': 'nueva',
'ver': 'ver',
'mostrar': 'ver',
'listar': 'ver',
'ls': 'ver',
'mias': 'ver',
'mías': 'ver',
'todas': 'ver',
'todos': 'ver',
'x': 'completar',
'hecho': 'completar',
'completar': 'completar',
'done': 'completar',
'tomar': 'tomar',
'claim': 'tomar',
'asumir': 'tomar',
'asumo': 'tomar',
'soltar': 'soltar',
'unassign': 'soltar',
'dejar': 'soltar',
'liberar': 'soltar',
'renunciar': 'soltar',
'ayuda': 'ayuda',
'help': 'ayuda',
'info': 'ayuda',
'?': 'ayuda',
'config': 'configurar',
'configurar': 'configurar',
'web': 'web'
};
export const SCOPE_ALIASES: Record<string, 'mis' | 'todos'> = {
'todo': 'todos',
'todos': 'todos',
'todas': 'todos',
'mis': 'mis',
'mias': 'mis',
'mías': 'mis',
'yo': 'mis'
};
/**
* Formatea un Date a YYYY-MM-DD respetando TZ (por defecto Europe/Madrid).
*/
export function ymdInTZ(d: Date, tz?: string): string {
const TZ = (tz && tz.trim()) || (process.env.TZ && process.env.TZ.trim()) || 'Europe/Madrid';
const parts = new Intl.DateTimeFormat('en-GB', {
timeZone: TZ,
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(d);
const get = (t: string) => parts.find(p => p.type === t)?.value || '';
return `${get('year')}-${get('month')}-${get('day')}`;
}
export function todayYMD(tz?: string): string {
return ymdInTZ(new Date(), tz);
}
/**
* Parsea múltiples IDs desde tokens, deduplica, y aplica límite.
*/
export function parseMultipleIds(tokens: string[], max: number = 10): { ids: number[]; truncated: boolean } {
const raw = (tokens || []).join(' ').trim();
const all = raw
? raw
.split(/[,\s]+/)
.map(t => t.trim())
.filter(Boolean)
.map(t => parseInt(t, 10))
.filter(n => Number.isFinite(n) && n > 0)
: [];
const dedup: number[] = [];
const seen = new Set<number>();
for (const n of all) {
if (!seen.has(n)) {
seen.add(n);
dedup.push(n);
}
}
const truncated = dedup.length > max;
const ids = dedup.slice(0, max);
return { ids, truncated };
}
/**
* Resuelve un ID de entrada (display_code) a task.id si está activa.
*/
export function resolveTaskIdFromInput(n: number): number | null {
const byCode = TaskService.getActiveTaskByDisplayCode(n);
return byCode ? byCode.id : null;
}
/**
* Aplica la política de membresía para acciones sobre una tarea.
* Devuelve true si el usuario está permitido según flags/env.
*/
export function enforceMembership(sender: string, task: { group_id?: string | null }, enforceFlag?: boolean): boolean {
const enforce =
typeof enforceFlag === 'boolean'
? enforceFlag
: String(process.env.GROUP_MEMBERS_ENFORCE || '').toLowerCase() === 'true';
const gid = task?.group_id || null;
if (!gid) return true; // tareas personales no requieren membresía
if (!enforce) return true;
if (!GroupSyncService.isSnapshotFresh(gid)) return true;
return GroupSyncService.isUserActiveInGroup(sender, gid);
}
Loading…
Cancel
Save