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|denyBenutzerfreundlichkeitsprobleme
- 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|denyPlattformvergleich
| Plattform | Genehmigungs-UX | Mechanismus |
|---|---|---|
| Discord | Ein-Klick-Inline-Buttons | discord.js ButtonComponents |
| Slack | Interaktive Buttons | Block Kit interaktive Buttons |
| Telegram | Manuelle Texteingabe | Keine 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 messageErwartete 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:
- Verifizieren Sie, dass die Nachricht drei distincte Codeblöcke (in Backticks eingeschlossen) enthält
- Tippen Sie auf jeden Codeblock — Telegram sollte die Option „Kopieren" anzeigen
- Kopieren und senden Sie jeden Befehl an den Bot
- 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 confirmationErwartete 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.08Testfall 3: Short-ID-Ablehnung verifizieren
Testbefehl:
# Manually send a short ID to verify the bot rejects it
/s approve 25395703 allow-onceErwartete 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:
| Zeichen | Maskiert | Zweck |
|---|---|---|
_ | _ | 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 escapingFehler 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:
| Plattform | Verhalten | Empfehlung |
|---|---|---|
| iOS | Langdrücken zeigt „Kopieren" | Funktioniert korrekt |
| Android | Langdrücken zeigt „Kopieren" | Funktioniert korrekt |
| Desktop macOS | Dreifachklick wählt aus | Funktioniert korrekt |
| Desktop Windows | Dreifachklick wählt aus | Funktioniert 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 insteadDies war die fehlgeschlagene Alternative, die im Issue erwähnt wurde. Die interne Notifier-Pipeline bleibt der korrekte Ansatz.
🔗 Zugehörige Fehler
Fehlerreferenztabelle
| Fehlercode | Beschreibung | Ursache | Lösung |
|---|---|---|---|
APPROVAL_001 | “Invalid approval ID format. Expected full UUID.” | Short ID oder malformed UUID an /approve übergeben | Sicherstellen, dass Nachricht vollständige UUID in kopierbarem Format enthält |
APPROVAL_002 | “Approval request expired” | Genehmigungs-TTL vor Entscheidung überschritten | Kürzeres TTL implementieren oder Benachrichtigungen erneuern |
APPROVAL_003 | “Approval not found” | UUID nicht im Genehmigungsspeicher | UUID wurde möglicherweise gelöscht; neue Genehmigung anfordern |
APPROVAL_004 | “Unauthorized approver” | Benutzer nicht auf Genehmigungs-Whitelist | Benutzer zum operator.approvals-Scope hinzufügen |
TG_001 | “Bot was blocked by the user” | Telegram-Bot kann Nachricht nicht zustellen | Benutzer muss Bot entsperren |
TG_002 | “Parse error: invalid JSON” | MarkdownV2-Escape-Fehler | Zeichenmaskierung überprüfen |
TG_003 | “Message is too long” | Kombinierte Nachricht überschreitet 4096 Zeichen | Nachricht kürzen oder aufteilen |
WS_001 | “Event exec.approval.requested not received” | Externe WS-Clients erhalten keine gescopten Events | Interne 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.