feat(gateway): add typing indicator for Telegram bot responses

Send "typing" chat action while LLM generates a response, with 5s
interval refresh and 60s safety timeout. Stop on message_end,
agent_error, or generic error events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-12 13:27:41 +08:00
parent 899a3d193c
commit f20143743b

View file

@ -53,8 +53,10 @@ const VERIFY_TIMEOUT_MS = 30_000;
@Injectable()
export class TelegramService implements OnModuleInit {
private static readonly TYPING_TIMEOUT_MS = 60_000; // 1 minute safety cap
private bot: Bot | null = null;
private pendingRequests = new Map<string, PendingRequest>();
private typingTimers = new Map<string, ReturnType<typeof setInterval>>();
private readonly logger = new Logger(TelegramService.name);
@ -351,12 +353,21 @@ export class TelegramService implements OnModuleInit {
return;
}
// Stream event — extract text content for Telegram
// Stream event — typing indicator + extract text content for Telegram
if (msg.action === StreamAction) {
const streamPayload = msg.payload as StreamPayload;
const event = streamPayload?.event;
if (event && "type" in event && event.type === "message_end") {
// Extract final text from the message
if (!event || !("type" in event)) return;
// Start typing when LLM begins generating
if (event.type === "message_start") {
this.startTyping(telegramUserId);
return;
}
// Stop typing + send text on message_end
if (event.type === "message_end") {
this.stopTyping(telegramUserId);
const agentMsg = (event as { message?: { content?: Array<{ type: string; text?: string }> } }).message;
if (agentMsg?.content) {
const textContent = agentMsg.content
@ -367,7 +378,15 @@ export class TelegramService implements OnModuleInit {
this.sendToTelegram(deviceId, textContent);
}
}
return;
}
// Stop typing on error
if (event.type === "agent_error") {
this.stopTyping(telegramUserId);
return;
}
return;
}
@ -382,6 +401,7 @@ export class TelegramService implements OnModuleInit {
// Error messages
if (msg.action === "error") {
this.stopTyping(telegramUserId);
const payload = msg.payload as { message?: string; code?: string };
if (payload?.message) {
this.sendToTelegram(deviceId, `Error: ${payload.message}`);
@ -391,6 +411,34 @@ export class TelegramService implements OnModuleInit {
});
}
/** Start sending "typing" indicator to Telegram at regular intervals */
private startTyping(telegramUserId: string): void {
if (this.typingTimers.has(telegramUserId)) return;
const chatId = Number(telegramUserId);
const send = () => {
void this.bot?.api.sendChatAction(chatId, "typing").catch(() => {});
};
send();
const interval = setInterval(send, 5000);
this.typingTimers.set(telegramUserId, interval);
// Safety timeout: auto-stop if no message_end/agent_error arrives
setTimeout(() => {
if (this.typingTimers.get(telegramUserId) === interval) {
this.stopTyping(telegramUserId);
}
}, TelegramService.TYPING_TIMEOUT_MS);
}
/** Stop the "typing" indicator for a Telegram user */
private stopTyping(telegramUserId: string): void {
const timer = this.typingTimers.get(telegramUserId);
if (timer) {
clearInterval(timer);
this.typingTimers.delete(telegramUserId);
}
}
/** Cleanup all pending requests (used on verify failure) */
private cleanupPendingRequests(): void {
for (const [id, pending] of this.pendingRequests) {