@gonzih/cc-tg 0.9.11 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bot.d.ts CHANGED
@@ -49,6 +49,7 @@ export declare class CcTgBot {
49
49
  */
50
50
  handleUserMessage(chatId: number, text: string): Promise<void>;
51
51
  private handleVoice;
52
+ private handleVoiceRetry;
52
53
  private handlePhoto;
53
54
  private handleDocument;
54
55
  private getOrCreateSession;
package/dist/bot.js CHANGED
@@ -31,6 +31,7 @@ const BOT_COMMANDS = [
31
31
  { command: "cost", description: "Show session token usage and cost" },
32
32
  { command: "skills", description: "List available Claude skills with descriptions" },
33
33
  { command: "cron", description: "Manage cron jobs — add/list/edit/remove/clear" },
34
+ { command: "voice_retry", description: "Retry failed voice message transcriptions" },
34
35
  ];
35
36
  const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
36
37
  const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
@@ -380,6 +381,11 @@ export class CcTgBot {
380
381
  await this.replyToChat(chatId, listSkills(), threadId);
381
382
  return;
382
383
  }
384
+ // /voice_retry — retry failed voice message transcriptions
385
+ if (text === "/voice_retry") {
386
+ await this.handleVoiceRetry(chatId, threadId);
387
+ return;
388
+ }
383
389
  const session = this.getOrCreateSession(chatId, threadId, threadName);
384
390
  try {
385
391
  const enriched = await enrichPromptWithUrls(text);
@@ -418,10 +424,24 @@ export class CcTgBot {
418
424
  return;
419
425
  console.log(`[voice:${chatId}] received voice message, transcribing...`);
420
426
  this.bot.sendChatAction(chatId, "typing", threadId !== undefined ? { message_thread_id: threadId } : undefined).catch(() => { });
427
+ // Store in Redis before transcription so we can retry on failure
428
+ const pendingEntry = JSON.stringify({
429
+ file_id: fileId,
430
+ chat_id: chatId,
431
+ message_id: msg.message_id,
432
+ timestamp: Date.now(),
433
+ });
434
+ if (this.redis) {
435
+ await this.redis.rpush("voice:pending", pendingEntry).catch((err) => console.warn("[voice] redis rpush voice:pending failed:", err.message));
436
+ }
421
437
  try {
422
438
  const fileLink = await this.bot.getFileLink(fileId);
423
439
  const transcript = await transcribeVoice(fileLink);
424
440
  console.log(`[voice:${chatId}] transcribed: ${transcript}`);
441
+ // Remove from pending on success
442
+ if (this.redis) {
443
+ await this.redis.lrem("voice:pending", 0, pendingEntry).catch((err) => console.warn("[voice] redis lrem voice:pending failed:", err.message));
444
+ }
425
445
  if (!transcript || transcript === "[empty transcription]") {
426
446
  await this.replyToChat(chatId, "Could not transcribe voice message.", threadId);
427
447
  return;
@@ -441,9 +461,97 @@ export class CcTgBot {
441
461
  }
442
462
  }
443
463
  catch (err) {
444
- console.error(`[voice:${chatId}] error:`, err.message);
445
- await this.replyToChat(chatId, `Voice transcription failed: ${err.message}`, threadId);
464
+ const errMsg = err.message;
465
+ console.error(`[voice:${chatId}] error:`, errMsg);
466
+ // Push to voice:failed on failure (entry stays in voice:pending for retry)
467
+ if (this.redis) {
468
+ const failedEntry = JSON.stringify({
469
+ file_id: fileId,
470
+ chat_id: chatId,
471
+ message_id: msg.message_id,
472
+ timestamp: Date.now(),
473
+ error: errMsg,
474
+ failed_at: Date.now(),
475
+ });
476
+ this.redis.rpush("voice:failed", failedEntry)
477
+ .then(() => this.redis.expire("voice:failed", 48 * 60 * 60))
478
+ .catch((redisErr) => console.warn("[voice] redis write voice:failed failed:", redisErr.message));
479
+ }
480
+ // User-friendly error messages
481
+ let userMsg;
482
+ if (errMsg.includes("whisper-cpp not found") || errMsg.includes("whisper not found")) {
483
+ userMsg = "Voice transcription unavailable — whisper-cpp not installed";
484
+ }
485
+ else if (errMsg.includes("No whisper model found")) {
486
+ userMsg = "Voice transcription unavailable — no whisper model found";
487
+ }
488
+ else if (errMsg.includes("HTTP") && errMsg.includes("downloading")) {
489
+ userMsg = "Could not download voice file from Telegram";
490
+ }
491
+ else {
492
+ userMsg = `Voice transcription failed: ${errMsg}`;
493
+ }
494
+ await this.replyToChat(chatId, userMsg, threadId);
495
+ }
496
+ }
497
+ async handleVoiceRetry(chatId, threadId) {
498
+ if (!this.redis) {
499
+ await this.replyToChat(chatId, "Redis not configured — voice retry unavailable.", threadId);
500
+ return;
501
+ }
502
+ const [pendingRaw, failedRaw] = await Promise.all([
503
+ this.redis.lrange("voice:pending", 0, -1).catch(() => []),
504
+ this.redis.lrange("voice:failed", 0, -1).catch(() => []),
505
+ ]);
506
+ // Deduplicate by file_id across both lists
507
+ const allEntries = new Map();
508
+ for (const raw of [...pendingRaw, ...failedRaw]) {
509
+ try {
510
+ const entry = JSON.parse(raw);
511
+ if (entry.file_id)
512
+ allEntries.set(entry.file_id, entry);
513
+ }
514
+ catch { /* skip malformed entries */ }
515
+ }
516
+ if (allEntries.size === 0) {
517
+ await this.replyToChat(chatId, "No pending voice messages to retry.", threadId);
518
+ return;
519
+ }
520
+ await this.replyToChat(chatId, `Retrying ${allEntries.size} voice message(s)...`, threadId);
521
+ let succeeded = 0;
522
+ let failed = 0;
523
+ const errors = [];
524
+ for (const [fileId, entry] of allEntries) {
525
+ try {
526
+ const fileLink = await this.bot.getFileLink(fileId);
527
+ const transcript = await transcribeVoice(fileLink);
528
+ if (transcript && transcript !== "[empty transcription]") {
529
+ const session = this.getOrCreateSession(entry.chat_id, threadId, undefined);
530
+ session.claude.sendPrompt(transcript);
531
+ this.writeChatMessage("user", "telegram", transcript, entry.chat_id);
532
+ // Remove from both lists
533
+ const matchPending = pendingRaw.find((r) => r.includes(`"${fileId}"`));
534
+ const matchFailed = failedRaw.find((r) => r.includes(`"${fileId}"`));
535
+ if (matchPending)
536
+ await this.redis.lrem("voice:pending", 0, matchPending).catch(() => { });
537
+ if (matchFailed)
538
+ await this.redis.lrem("voice:failed", 0, matchFailed).catch(() => { });
539
+ succeeded++;
540
+ }
541
+ else {
542
+ failed++;
543
+ errors.push(`${fileId}: empty transcription`);
544
+ }
545
+ }
546
+ catch (err) {
547
+ failed++;
548
+ errors.push(`${fileId}: ${err.message}`);
549
+ }
446
550
  }
551
+ const lines = [`Voice retry complete: ${succeeded} succeeded, ${failed} failed.`];
552
+ if (errors.length > 0)
553
+ lines.push(...errors.map((e) => `• ${e}`));
554
+ await this.replyToChat(chatId, lines.join("\n"), threadId);
447
555
  }
448
556
  async handlePhoto(chatId, msg, threadId, threadName) {
449
557
  // Pick highest resolution photo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {