April 19, 2026 • Versión: latest

Agregar comandos de aprobación Tap-to-Copy a los mensajes de aprobación exec de Telegram

Cómo implementar botones de comando /approve listos para copiar en las notificaciones de aprobación exec de Telegram para eliminar copiar UUIDs manualmente en dispositivos móviles.

🔍 Síntomas

Mensaje actual de aprobación exec en Telegram

Cuando se activa una aprobación exec, el bot de Telegram envía:

🔒 Exec approval required
ID: 25395703-a97b-4bc0-8f20-52701089a058
Command: uptime
Triggered by: [email protected]
Timestamp: 2024-01-15T10:30:00Z

Reply with: /approve <id> allow-once|allow-always|deny

Problemas de experiencia de usuario

  • Copiado manual del UUID — Los usuarios deben mantener presionado para seleccionar el UUID, lo que en dispositivos móviles a menudo resulta en una selección incorrecta debido a la longitud del UUID y la ubicación de los guiones
  • Construcción del comando — Después de copiar, los usuarios deben escribir manualmente la estructura del comando: /approve + espacio + pegar UUID + espacio + acción
  • Alta tasa de errores — Un solo carácter mal escrito provoca el rechazo del comando, requiriendo reiniciar todo el proceso
  • Fricción en móvil — El flujo de trabajo requiere 8-12 interacciones manuales en lugar de un solo toque

Respuesta de error observada

Cuando un usuario envía un comando de aprobación con formato incorrecto:

Invalid approval ID format. Expected full UUID.
Use: /approve <uuid> allow-once|allow-always|deny

Comparación de plataformas

PlataformaUX de aprobaciónMecanismo
DiscordBotones en línea con un clicdiscord.js ButtonComponents
SlackBotones interactivosBlock Kit interactive buttons
TelegramEntrada de texto manualSin soporte nativo de botones en este contexto

🧠 Causa raíz

Análisis de arquitectura técnica

El flujo de trabajo de aprobación exec de Telegram involucra múltiples componentes:

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Exec Tool      │────▶│  Approval Queue  │────▶│  Telegram       │
│  (agent/core)   │     │  (approval svc)  │     │  Notifier       │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │  approval ID     │
                        │  (full UUID)     │
                        └──────────────────┘

Factores de causa raíz

1. Limitación de formato de mensaje de Telegram

Telegram soporta modos de análisis markdownv2 y HTML, pero no soporta botones de teclado en línea cuando el bot recibe mensajes (solo cuando el bot envía mensajes proactivos). El enfoque ReplyKeyboardMarkup no es adecuado para flujos de trabajo de copiar y pegar.

2. Deficiencia en la plantilla de mensaje de código

En packages/notifier-telegram/src/lib/format-message.ts (o equivalente), la plantilla del mensaje de aprobación construye el texto legible pero omite los bloques de comando listos para usar:

// Current implementation (simplified)
const formatApprovalMessage = (approval) => {
  return [
    '🔒 Exec approval required',
    `ID: ${approval.id}`,
    `Command: ${approval.command}`,
    '',
    'Reply with: /approve <id> allow-once|allow-always|deny'
  ].join('\n');
};

La plantilla requiere que los usuarios extraigan y reconstruyan manualmente el comando.

3. No soporte de ID corto

El manejador del comando /approve solo acepta UUIDs completos, no prefijos cortos:

// Command handler validates full UUID
const APPROVAL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

if (!APPROVAL_ID_PATTERN.test(approvalId)) {
  throw new Error('Invalid approval ID format. Expected full UUID.');
}

Esta decisión de diseño impide que los usuarios usen el prefijo corto visible en el mensaje.

4. Alternativa fallida: Suscripción a eventos WebSocket

Se intentó un enfoque alternativo usando la conexión WebSocket de la puerta de enlace:

// Attempted external notifier approach
gateway.ws.on('exec.approval.requested', (event) => {
  // Send Telegram message with copyable commands
  await telegram.send({
    text: buildApprovalMessage(event),
    parse_mode: 'MarkdownV2'
  });
});

// Result: Event never received by external clients
// Root cause: exec\.approval\.requested events not broadcast to
// clients with operator.approvals scope (possible bug in event routing)

El evento con ámbito no se propagaba a suscriptores externos, eliminando este enfoque.

🛠️ Solución paso a paso

Fase 1: Modificar el formateador de mensajes

Archivo: packages/notifier-telegram/src/lib/format-message.ts

Antes:

export function formatExecApprovalMessage(
  approval: ExecApproval
): string {
  const lines = [
    '🔒 Exec approval required',
    `ID: ${approval.id}`,
    `Command: ${approval.command}`,
    `Triggered by: ${approval.triggeredBy}`,
    `Timestamp: ${approval.timestamp}`,
    '',
    'Reply with: /approve  allow-once|allow-always|deny'
  ];
  
  return lines.join('\n');
}

Después:

export function formatExecApprovalMessage(
  approval: ExecApproval
): string {
  // Escape special characters for Telegram MarkdownV2
  const escapeMarkdownV2 = (text: string): string => {
    const specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
    return specialChars.reduce((str, char) => str.replace(char, `\\${char}`), text);
  };

  const escapedId = escapeMarkdownV2(approval.id);
  const escapedCommand = escapeMarkdownV2(approval.command);
  const escapedUser = escapeMarkdownV2(approval.triggeredBy);
  
  const lines = [
    '🔒 *Exec approval required*',
    '',
    `\\- \\*ID\\:* \`${escapedId}\``,
    `\\- \\*Command\\:* \`${escapedCommand}\``,
    `\\- \\*Triggered by\\:* ${escapedUser}`,
    `\\- \\*Timestamp\\:* ${approval.timestamp}`,
    '',
    '*Tap to copy and send one of:*{ESCAPED_BREAK_POINT}',
    '',
    '✅ Tap to approve once:',
    `\`/approve ${escapedId} allow\\-once\``,
    '',
    '♾️ Tap to approve always:',
    `\`/approve ${escapedId} allow\\-always\``,
    '',
    '⛔ Tap to deny:',
    `\`/approve ${escapedId} deny\``,
  ];
  
  return lines.join('\n');
}

Nota: En MarkdownV2, los saltos de línea dentro de los bloques de código impiden el copiar y pegar. Cada comando debe estar en su propia línea. Los comandos envueltos en acentos graves aseguran que Telegram los renderice como bloques de código tocables.

Fase 2: Actualizar opciones de envío de Telegram

Archivo: packages/notifier-telegram/src/lib/send-message.ts

Antes:

const sendMessage = async (chatId: string, text: string) => {
  await telegramBot.sendMessage(chatId, text, {
    parse_mode: 'Markdown'
  });
};

Después:

const sendMessage = async (chatId: string, text: string) => {
  // Replace placeholder with actual line break (MarkdownV2 compatible)
  const processedText = text.replace(
    /\{ESCAPED_BREAK_POINT\}/g,
    '\\- \\- \\-'
  );
  
  await telegramBot.sendMessage(chatId, processedText, {
    parse_mode: 'MarkdownV2',
    disable_web_page_preview: true
  });
};

Fase 3: Actualización de definición de tipo

Archivo: packages/notifier-telegram/src/types/approval.ts

export interface ExecApproval {
  id: string;              // Full UUID (required for /approve command)
  command: string;         // The exec command being approved
  triggeredBy: string;     // Operator/username who triggered
  timestamp: string;       // ISO 8601 timestamp
  status?: 'pending' | 'approved' | 'denied';
  expiresAt?: string;      // Optional expiration
}

Fase 4: Verificar disponibilidad del UUID

Asegurar que el ID de aprobación pasado al formateador siempre sea el UUID completo:

Archivo: packages/notifier-telegram/src/lib/handle-approval.ts

import { validateUUID } from '@openclaw/shared-utils';

// Ensure we're always working with full UUIDs
const ensureFullUUID = (id: string): string => {
  if (!validateUUID(id)) {
    throw new Error(
      `Invalid approval ID: ${id}. Short IDs cannot be used with /approve command.`
    );
  }
  return id;
};

export async function handleApprovalRequest(approval: RawApproval) {
  const fullUUID = ensureFullUUID(approval.approvalId);
  
  const message = formatExecApprovalMessage({
    id: fullUUID,
    command: approval.command,
    triggeredBy: approval.triggeredBy,
    timestamp: approval.timestamp,
  });
  
  await sendApprovalNotification(approval.chatId, message);
}

🧪 Verificación

Caso de prueba 1: El mensaje se renderiza con comandos copiables

Comando de prueba CLI:

# Start the bot and trigger an exec approval
openclaw exec --agent=prod-01 "uptime" --require-approval

# In a separate Telegram chat with the bot, observe the message

Salida esperada en Telegram:

🔒 *Exec approval required*

\- *ID:* `25395703-a97b-4bc0-8f20-52701089a058`
\- *Command:* `uptime`
\- *Triggered by:* [email protected]
\- *Timestamp:* 2024-01-15T10:30:00Z

*Tap to copy and send one of:*
\- \- \-

✅ Tap to approve once:
`/approve 25395703-a97b-4bc0-8f20-52701089a058 allow-once`

♾️ Tap to approve always:
`/approve 25395703-a97b-4bc0-8f20-52701089a058 allow-always`

⛔ Tap to deny:
`/approve 25395703-a97b-4bc0-8f20-52701089a058 deny`

Pasos de verificación:

  1. Verificar que el mensaje contiene tres bloques de código distintos (envueltos en acentos graves)
  2. Tocar cada bloque de código — Telegram debería mostrar la opción "Copiar"
  3. Copiar y enviar cada comando al bot
  4. Confirmar que cada comando es aceptado sin error de "Invalid ID format"

Caso de prueba 2: Ejecución de comando después de la aprobación

Comando de prueba CLI:

# After sending allow-once approval via Telegram
openclaw exec --agent=prod-01 "uptime" --require-approval

# User in Telegram taps the first command block, copies, sends
# Expected: Bot responds with approval confirmation

Respuesta esperada del bot:

✅ Approved exec request (one\\-time)
Command: uptime
Agent: prod\\-01
Status: Executing...
10:30:05 up 23 days, 4:12, 2 users, load average: 0.15, 0.10, 0.08

Caso de prueba 3: Verificar rechazo de ID corto

Comando de prueba:

# Manually send a short ID to verify the bot rejects it
/s approve 25395703 allow-once

Respuesta esperada:

❌ Invalid approval ID format.

The /approve command requires the full UUID.
Short IDs or partial IDs are not supported.

Use: /approve <uuid> allow-once|allow-always|deny

💡 Tip: Tap the command in the approval message to copy it directly.

Verificación de pruebas unitarias

# Run the notifier-telegram unit tests
cd packages/notifier-telegram
npm test -- --testPathPattern="format-message"

# Expected output:
# ✓ formatExecApprovalMessage includes copyable commands
# ✓ formatExecApprovalMessage escapes special characters
# ✓ formatExecApprovalMessage handles long commands
# ✓ formatExecApprovalMessage handles special characters in command

⚠️ Errores comunes

Error común 1: Omisiones de escape en MarkdownV2

El analizador de MarkdownV2 de Telegram es estricto. Los caracteres especiales sin escapar rompen todo el mensaje.

Errores comunes:

// ❌ WRONG: Unescaped parentheses and hyphens
`/approve ${id} allow-once`

// ✅ CORRECT: Escaped special characters
`/approve ${escapedId} allow\\-once`

// ❌ WRONG: Unescaped dots in UUID context
`command: uptime`

// ✅ CORRECT: Escaped if using MarkdownV2 bold
`\\*Command\\:* \`uptime\``

Tabla de referencia de escape:

CarácterCon escapePropósito
__Marcadores de cursiva
**Marcadores de negrita
``Bloques de código
((Marcadores de enlace/formato
))Marcadores de enlace/formato
--Marcadores de lista
..Puede romper el análisis
|

Error común 2: Comando en la misma línea que el texto

❌ INCORRECTO:

✅ Tap to approve: `/approve ${id} allow-once`

✅ CORRECTO:

✅ Tap to approve once:
`/approve ${id} allow-once`

Telegram requiere que el bloque de código esté en su propia línea para habilitar la funcionalidad de tocar para copiar.

Error común 3: Usar modo de análisis de Markdown heredado

❌ INCORRECTO:

parse_mode: 'Markdown'  // Legacy, deprecated

✅ CORRECTO:

parse_mode: 'MarkdownV2'  // Current, required for proper escaping

Error común 4: Inyección de comandos en el ID de aprobación

Nunca insertar IDs de aprobación sin sanitizar directamente en los mensajes:

// ❌ DANGEROUS: Unsanitized ID could break parsing
`/approve ${approval.id} deny`

// ✅ SAFE: ID validated before message construction
const safeId = validateAndSanitizeUUID(approval.id);
`/approve ${safeId} deny`

Error común 5: Copiar y pegar en escritorio vs móvil

Los clientes de Telegram de escritorio manejan el tocar para copiar de bloques de código de manera diferente a los móviles:

PlataformaComportamientoRecomendación
iOSMantener presionado muestra “Copiar”Funciona correctamente
AndroidMantener presionado muestra “Copiar”Funciona correctamente
Desktop macOSTriple clic seleccionaFunciona correctamente
Desktop WindowsTriple clic seleccionaFunciona correctamente

Siempre probar en dispositivos móviles como caso de uso principal.

Error común 6: Límites de longitud del mensaje

Los mensajes de Telegram están limitados a 4096 caracteres. Los comandos largos con UUIDs completos pueden acercarse a este límite:

const MAX_MESSAGE_LENGTH = 4096;

if (formattedMessage.length > MAX_MESSAGE_LENGTH) {
  // Truncate command display or use abbreviated format
  logger.warn('Approval message exceeds Telegram length limit', {
    approvalId: approval.id,
    messageLength: formattedMessage.length
  });
}

Error común 7: Suscripción a eventos WebSocket (Histórico)

Al implementar enfoques alternativos de notificación:

// ❌ Attempting to subscribe to approval events via gateway WS
gateway.subscribe('exec.approval.requested', handler);

// Result: Event not received
// Reason: exec.* events are not broadcast to external scope subscribers
// Workaround: Use internal notifier pipeline instead

Esta fue la alternativa fallida mencionada en el issue. La tubería de notificación interna sigue siendo el enfoque correcto.

🔗 Errores relacionados

Tabla de referencia de errores

Código de errorDescripciónCausaResolución
APPROVAL_001“Invalid approval ID format. Expected full UUID.”ID corto o UUID mal formado pasado a /approveAsegurar que el mensaje incluya el UUID completo en formato copiable
APPROVAL_002“Approval request expired”TTL de aprobación excedido antes de la decisiónImplementar TTL más corto o notificaciones de renovación
APPROVAL_003“Approval not found”UUID no está en el almacén de aprobacionesEl UUID puede haber sido limpiado; solicitar nueva aprobación
APPROVAL_004“Unauthorized approver”Usuario no está en la lista blanca de aprobaciónAgregar usuario al ámbito operator.approvals
TG_001“Bot was blocked by the user”El bot de Telegram no puede entregar el mensajeEl usuario debe desbloquear el bot
TG_002“Parse error: invalid JSON”Error de escape de MarkdownV2Revisar el escape de caracteres
TG_003“Message is too long”Mensaje combinado excede 4096 caracteresTruncar o dividir el mensaje
WS_001“Event exec.approval.requested not received”Los clientes WS externos no reciben eventos con ámbitoUsar tubería de notificación interna (ver Error común 7)

Issues de GitHub relacionados

  • #2147 — "Telegram inline keyboard support for approval actions" (cerrado, diferido — limitaciones de Telegram)
  • #1893 — "exec.approval.requested event not broadcast to external WebSocket clients" (abierto — posible bug)
  • #1756 — "Short ID support for /approve command" (cerrado, no se implementará — preocupación de seguridad)
  • #1522 — "Discord approval buttons UX inconsistency with other channels" (resuelto)

Consideraciones de seguridad

El comando /approve requiere UUID completo para prevenir:

  • Ataques de enumeración en IDs de aprobación
  • Adivinanza por fuerza bruta de IDs de aprobación
  • Elusión de autorización a través de colisión de ID corto

El enfoque de UUID completo asegura que los enlaces de aprobación no puedan ser predichos o cosechados.

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.