April 19, 2026 • Version: latest

Hinzufügen von kopierbaren Genehmigungsbefehlen zu Telegram Exec-Genehmigungsnachrichten

Wie man kopierbare /approve Befehlsschaltflächen in Telegram Exec-Genehmigungsbenachrichtigungen implementiert, um manuelles UUID-Kopieren auf Mobilgeräten zu eliminieren.

🔍 Symptome

Aktuelle Telegram-Exec-Genehmigungsnachricht

Wenn eine Exec-Genehmigung ausgelöst wird, sendet der Telegram-Bot:

🔒 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

Benutzerfreundlichkeitsprobleme

  • Manuelles UUID-Kopieren — Benutzer müssen lange drücken, um die UUID auszuwählen, was auf Mobilgeräten aufgrund der UUID-Länge und Bindestrich-Platzierung häufig zu falscher Auswahl führt
  • Befehlskonstruktion — Nach dem Kopieren müssen Benutzer die Befehlsstruktur manuell eingeben: /approve + Leerzeichen + UUID einfügen + Leerzeichen + Aktion
  • Hohe Fehlerquote — Ein falsch eingegebenes Zeichen führt zur Ablehnung des Befehls, was einen Neustart des gesamten Prozesses erfordert
  • Mobilreibung — Der Workflow erfordert 8-12 manuelle Interaktionen anstelle eines einzigen Tippens

Beobachtete Fehlerantwort

Wenn ein Benutzer einen falsch formatierten Genehmigungsbefehl sendet:

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

Plattformvergleich

PlattformGenehmigungs-UXMechanismus
DiscordEin-Klick-Inline-Buttonsdiscord.js ButtonComponents
SlackInteraktive ButtonsBlock Kit interaktive Buttons
TelegramManuelle TexteingabeKeine nativen Button-Unterstützung in diesem Kontext

🧠 Ursache

Technische Architekturanalyse

Der Telegram-Exec-Genehmigungsworkflow umfasst mehrere Komponenten:

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

Ursachenfaktoren

1. Telegram-Nachrichtenformatierungslimitierung

Telegram unterstützt markdownv2- und HTML-Parse-Modi, aber keine Inline-Tastatur-Buttons wenn der Bot Nachrichten empfängt (nur wenn der Bot proaktiv Nachrichten sendet). Der ReplyKeyboardMarkup-Ansatz eignet sich nicht für Inline-Kopier-Einfügen-Workflows.

2. Codenachrichten-Template-Mangel

In packages/notifier-telegram/src/lib/format-message.ts (oder Äquivalent) konstruiert das Genehmigungsnachrichten-Template den menschenlesbaren Text,省略t aber die sofort verwendbaren Befehlsblöcke:

// 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');
};

Das Template erfordert, dass Benutzer den Befehl manuell extrahieren und rekonstruieren.

3. Keine Short-ID-Unterstützung

Der /approve-Befehlshandler akzeptiert nur vollständige UUIDs, keine Kurzpräfixe:

// 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.');
}

Diese Designentscheidung verhindert, dass Benutzer das sichtbare Kurzpräfix in der Nachricht verwenden.

4. Fehlgeschlagene Alternative: WebSocket-Event-Abonnement

Ein alternativer Ansatz wurde über die Gateway-WebSocket-Verbindung versucht:

// 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)

Das gescopte Event wurde nicht an externe Abonnenten weitergegeben, was diesen Ansatz ausschließt.

🛠️ Schritt-für-Schritt-Lösung

Phase 1: Den Nachrichtenformatierer ändern

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

Vorher:

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');
}

Nachher:

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');
}

Hinweis: In MarkdownV2 verhindern Zeilenumbrüche innerhalb von Codeblöcken das Kopieren-Einfügen. Jeder Befehl muss in einer eigenen Zeile sein. Die in Backticks eingeschlossenen Befehle stellen sicher, dass Telegram sie als tippbare Codeblöcke rendert.

Phase 2: Telegram-Sendeoptionen aktualisieren

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

Vorher:

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

Nachher:

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
  });
};

Phase 3: Typdefinition aktualisieren

Datei: 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
}

Phase 4: UUID-Verfügbarkeit verifizieren

Stellen Sie sicher, dass die an den Formatierer übergebene Genehmigungs-ID immer die vollständige UUID ist:

Datei: 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);
}

🧪 Verifizierung

Testfall 1: Nachricht rendert mit kopierbaren Befehlen

CLI-Testbefehl:

# 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

Erwartete Telegram-Ausgabe:

🔒 *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`

Verifizierungsschritte:

  1. Verifizieren Sie, dass die Nachricht drei distincte Codeblöcke (in Backticks eingeschlossen) enthält
  2. Tippen Sie auf jeden Codeblock — Telegram sollte die Option „Kopieren" anzeigen
  3. Kopieren und senden Sie jeden Befehl an den Bot
  4. Bestätigen Sie, dass jeder Befehl ohne „Invalid ID format"-Fehler akzeptiert wird

Testfall 2: Befehlsausführung nach Genehmigung

CLI-Testbefehl:

# 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

Erwartete Bot-Antwort:

✅ 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

Testfall 3: Short-ID-Ablehnung verifizieren

Testbefehl:

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

Erwartete Antwort:

❌ 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.

Unittest-Verifizierung

# 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

⚠️ Häufige Fehler

Fehler 1: MarkdownV2-Escape-Übersehungen

Telegrams MarkdownV2-Parser ist strikt. Nicht maskierte Sonderzeichen unterbrechen die gesamte Nachricht.

Häufige Fehler:

// ❌ 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\``

Escape-Referenztabelle:

ZeichenMaskiertZweck
__Kursivmarker
**Fettmarker
``Codeblöcke
((Link/Formatmarker
))Link/Formatmarker
--Listenmarker
..Kann Parsing unterbrechen
|

Fehler 2: Befehl in derselben Zeile wie Text

❌ FALSCH:

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

✅ RICHTIG:

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

Telegram erfordert, dass der Codeblock in einer eigenen Zeile ist, um die Tippen-zum-Kopieren-Funktionalität zu aktivieren.

Fehler 3: Legacy-Markdown-Parse-Modus verwenden

❌ FALSCH:

parse_mode: 'Markdown'  // Legacy, deprecated

✅ RICHTIG:

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

Fehler 4: Befehlsinjektion in Genehmigungs-ID

Betten Sie niemals nicht bereinigte Genehmigungs-IDs direkt in Nachrichten ein:

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

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

Fehler 5: Kopieren-Einfügen auf Desktop vs. Mobil

Desktop-Telegram-Clients behandeln Codeblock-Tippen-zum-Kopieren anders als Mobilgeräte:

PlattformVerhaltenEmpfehlung
iOSLangdrücken zeigt „Kopieren"Funktioniert korrekt
AndroidLangdrücken zeigt „Kopieren"Funktioniert korrekt
Desktop macOSDreifachklick wählt ausFunktioniert korrekt
Desktop WindowsDreifachklick wählt ausFunktioniert korrekt

Testen Sie immer auf Mobilgeräten als primärem Anwendungsfall.

Fehler 6: Nachrichtenlängenlimit

Telegram-Nachrichten sind auf 4096 Zeichen begrenzt. Lange Befehle mit vollständigen UUIDs können dieses Limit erreichen:

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
  });
}

Fehler 7: WebSocket-Event-Abonnement (historisch)

Bei der Implementierung alternativer Benachrichtigungsansätze:

// ❌ 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

Dies war die fehlgeschlagene Alternative, die im Issue erwähnt wurde. Die interne Notifier-Pipeline bleibt der korrekte Ansatz.

🔗 Zugehörige Fehler

Fehlerreferenztabelle

FehlercodeBeschreibungUrsacheLösung
APPROVAL_001“Invalid approval ID format. Expected full UUID.”Short ID oder malformed UUID an /approve übergebenSicherstellen, dass Nachricht vollständige UUID in kopierbarem Format enthält
APPROVAL_002“Approval request expired”Genehmigungs-TTL vor Entscheidung überschrittenKürzeres TTL implementieren oder Benachrichtigungen erneuern
APPROVAL_003“Approval not found”UUID nicht im GenehmigungsspeicherUUID wurde möglicherweise gelöscht; neue Genehmigung anfordern
APPROVAL_004“Unauthorized approver”Benutzer nicht auf Genehmigungs-WhitelistBenutzer zum operator.approvals-Scope hinzufügen
TG_001“Bot was blocked by the user”Telegram-Bot kann Nachricht nicht zustellenBenutzer muss Bot entsperren
TG_002“Parse error: invalid JSON”MarkdownV2-Escape-FehlerZeichenmaskierung überprüfen
TG_003“Message is too long”Kombinierte Nachricht überschreitet 4096 ZeichenNachricht kürzen oder aufteilen
WS_001“Event exec.approval.requested not received”Externe WS-Clients erhalten keine gescopten EventsInterne Notifier-Pipeline verwenden (siehe Fehler 7)

Zugehörige GitHub-Issues

  • #2147 — "Telegram inline keyboard support for approval actions" (geschlossen, zurückgestellt — Telegram-Limitierungen)
  • #1893 — "exec.approval.requested event not broadcast to external WebSocket clients" (offen — möglicher Bug)
  • #1756 — "Short ID support for /approve command" (geschlossen, won't fix — Sicherheitsbedenken)
  • #1522 — "Discord approval buttons UX inconsistency with other channels" (gelöst)

Sicherheitsüberlegungen

Der /approve-Befehl erfordert vollständige UUID, um zu verhindern:

  • Enumerationsangriffe auf Genehmigungs-IDs
  • Brute-Force-Raten von Genehmigungs-IDs
  • Autorisierungsumgehung durch Short-ID-Kollision

Der vollständige UUID-Ansatz stellt sicher, dass Genehmigungslinks nicht vorhergesagt oder geerntet werden können.

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.