Меня зовут Сол ГудКод. Обычно я помогаю людям выпутываться из ситуаций, в которые они сами себя загнали: кто-то подписал не тот договор, кто-то удалил не ту ветку. Но параллельно надо было успевать работать с Claude Code, а он, зараза, живёт в терминале на моём столе.
Моя личная ситуация была такая. Задачи приходят в голову не за столом, а в дороге, в очереди, в лифте. К компьютеру я сажусь через четыре часа, когда мысль уже выветрилась, а формулировка усохла до «надо там что-то поправить».
Мне нужен был бот, которому можно наговорить задачу голосом, а он бы её сделал. С моими файлами, в моих проектах, на моей машине.
Дальше вышло длиннее, чем я рассчитывал.
Хорошая новость: у Anthropic это есть официально
Первое, что я выяснил: изобретать ничего не надо. У Anthropic есть официальный Telegram-плагин в рамках фичи Channels. Сообщения из чата пушатся прямо в живую сессию Claude Code.
Более того, плагин умеет больше, чем написано в его README. Я полез читать server.ts и нашёл там обработчики message:voice и message:audio, объявленную возможность claude/channel/permission и инлайн-кнопки подтверждения прав. В README про это ни слова. Мораль: читайте исходники, а не описания. README устаревает первым.
Установка заняла минут пятнадцать. BotFather, токен, /plugin install telegram@claude-plugins-official, перезапуск с флагом --channels, паринг.
Написал боту.
Тишина.
Подсудимый выглядит виновным
Дальше начался самый неприятный сорт отладки: тот, где всё исправно, а результата нет.
Проверил процессы. Сессия Claude Code запущена, флаг --channels на месте. Поллера плагина нет. Ладно, думаю, значит MCP-сервер не поднялся. Полез разбираться, почему.
claude mcp list
plugin:telegram:telegram: bun run --cwd ... start - √ Connected
Connected. Сервер подключается нормально.
Тогда я попросил Claude в headless-режиме позвать инструмент reply у этого сервера напрямую. Через несколько секунд мне в Telegram пришло сообщение «проверка связи из Claude Code». То есть исходящий путь полностью рабочий: бот может говорить.
Проверил, доходят ли до бота мои сообщения. Дёрнул getUpdates руками:
ok=True, накопленных апдейтов: 4
chat_id=1037166722 тип=private text=/start
chat_id=1037166722 тип=private text=asdasd
chat_id=-100... тип=supergroup text=Привет
Вот они, все мои сообщения, лежат в очереди. Бот их получил. Некому было забрать.
Сложите картину. MCP-сервер подключён, инструменты работают, порт слушается, сообщения до бота доходят, в логах чисто. И при этом события канала в сессию не попадают. Ни ошибки, ни предупреждения — ничего.
Я потратил на это часа полтора, прежде чем понял, что ищу не там.
Как проверить за минуту то, на что я убил вечер
Ключ оказался в документации Channels, в разделе про политики организации. Там прямым текстом: если каналы выключены, «MCP-сервер всё равно подключится и его инструменты будут работать, но сообщения канала не придут».
Это ровно мой отпечаток. Один в один.
Чтобы убедиться, я поставил официальный демо-канал fakechat. Он поднимает веб-интерфейс на localhost, никакого Telegram, никаких токенов. Запустил сессию с --channels plugin:fakechat@..., отправил на 127.0.0.1:8787 задачу «создай файл вот тут».
Порт слушался. Запрос принят, 204. Восемь процессов bun живы.
Файл не появился.
Если у вас похожая тишина, начните с этого теста. Он занимает минуту и сразу отвечает на вопрос «дело в моём боте или в чём-то системном».
Приговор: тумблер, которого у меня нет
Причина была на стартовом баннере, где я её сто раз видел и ни разу не прочитал:
Opus 5 with high effort · Claude Team · Digitalbasistax
Claude Team. А Channels — это research preview, и на планах Team и Enterprise они выключены по умолчанию, пока владелец организации не включит тумблер в админке. Локально это не обходится: channelsEnabled относится к managed-настройкам, пользователь их не переопределяет.
Админских прав у меня нет.
За двадцать лет практики я к такому привык: клиент невиновен, доказательства на его стороне, а дело всё равно не движется, потому что нужную бумагу подписывает человек, которого нет в городе. Технически всё в порядке. Практически ничего не работает.
Я честно рассмотрел законные пути. Попросить владельца организации: один тумблер, тридцать секунд, бесплатно. Личный аккаунт Pro или Max вне организации, где проверки политики просто не применяются. Ключ Anthropic Console, потому что при аутентификации по API-ключу каналы разрешены по умолчанию, только платить будешь по токенам вместо места в подписке.
И на этом я почти закрыл вопрос. А потом сообразил, что рассуждаю как человек, который перепутал транспорт с возможностью.
Апелляция: а зачем мне вообще их канал
Сторонние решения для Claude Code в Telegram появились задолго до Channels. Они никакой фичи Anthropic не используют. Они опрашивают Bot API сами и запускают Claude Code headless-вызовами.
Тумблер организации гасит конкретный механизм доставки. Способность запускать claude -p он не гасит.
Дальше выяснилось, что в CLI есть всё, что нужно для полноценного чата:
claude -p "задача" --session-id <uuid> --output-format stream-json --verbose
claude -p "уточнение" --resume <uuid>
Проверил --resume отдельным тестом, потому что не верил: попросил запомнить слово, следующим вызовом спросил, какое. Вспомнил. Контекст переносится между вызовами, а значит чат ведёт себя как чат, а не как набор изолированных запросов.
И тут я поймал приятную мысль: у моста получается свойство, которого у официального канала нет. Channels доставляют события только в уже запущенную сессию, то есть окно терминала должно висеть открытым. Мосту это не нужно. Он поднимает сессию под каждое сообщение и продолжает её по id.
Схема вышла такая:
Telegram Bot API ──long poll──▶ мост
│ гейт по ID отправителя
│ голос → Whisper → текст
│ файлы → inbox, пути в промпт
▼
claude -p --resume <session-id>
--output-format stream-json
│ события инструментов → прогресс
▼
Telegram sendMessage / sendDocument
Три файла на TypeScript под Bun: клиент Bot API, запуск и разбор потока событий, основной цикл. Плюс скрипт расшифровки на Python.
Улики, которые стоили мне вечера
Вот эта часть, по-моему, ценнее самого моста. Код вы напишете свой, а грабли тут общие.
Токен, который потерялся молча
Записал токен в .env командой Set-Content. Плагин ответил, что токена нет.
Файл на месте, токен в нём, глазами видно. Воспроизвёл логику парсинга построчно и увидел: регулярка ^(w+)=(.*)$ применяется после split('n'). Set-Content в Windows пишет CRLF, поэтому в конце строки остаётся r. А в JavaScript точка не матчит r, и $ перед ним не встаёт.
Строка не матчится. Токен теряется. И всё это внутри пустого catch {}, так что ошибки вы не увидите.
Пишите .env только с LF.
Кириллица в PowerShell 5.1
PowerShell 5.1 читает .ps1 в системной ANSI-кодировке, если в файле нет BOM. Кириллица в UTF-8 без BOM превращается в мусор, и скрипт падает не на логике, а на парсинге: «The string is missing the terminator» на строке с русским комментарием.
Отдельная подлость в том, что я наступил на это дважды. Второй раз внутри скрипта, у которого вывод был подавлен. Выглядело как «функция отмены просто не работает». Никаких ошибок, ничего в логе, отмена не отменяет.
Теперь у меня рефлекс: если у скрипта подавлен вывод и он «ничего не делает», первым делом смотрю первые три байта файла. Должно быть 239,187,191.
Промпт, обрезанный по первому пробелу
Мой любимый. Мост запускал Claude через spawn с shell: true, потому что claude в PATH это claude.ps1, а его без шелла не запустить.
С shell: true Node склеивает аргументы через пробел и отдаёт строку в cmd.exe без квотинга. Промпт разваливается по первому же пробелу.
Я отправил голосовое «Скажи, у тебя IP-адрес RoseVPN есть или нет?». Расшифровка сработала идеально. Ответ пришёл такой:
Слушаю — что сказать? Договори мысль, а то приходят обрывки.
Он получил слово «Скажи,» и вежливо попросил закончить фразу. Полторы минуты я тупо смотрел в этот ответ, прежде чем понял, что он абсолютно прав.
Лечится запуском claude.exe напрямую с shell: false, а путь к exe приходится резолвить самому. Та же беда, кстати, у Start-Process -ArgumentList с массивом: элементы не квотируются. На эти же грабли я успел наступить раньше, в PowerShell, и всё равно повторил в своём же коде.
Whisper, который не знает ваших продуктов
Расшифровка русского на локальном faster-whisper работает прилично. Голосовуха на 2,4 секунды обрабатывается за 4,3 секунды на RTX 4060, из которых большая часть уходит на загрузку модели с диска. Первый прогон занял 181 секунду, но там качалась сама модель на 1,6 ГБ.
Проблема в латинице внутри русской речи. «RoseVPN» превратился в «розовый пн». Не бессмыслица, а вполне правдоподобное словосочетание, которое ломает смысл фразы.
Лечится параметром initial_prompt: туда кладётся короткий список ваших продуктов и технологий, и декодирование смещается в их сторону. После этого получилось «IP-адрес RoseVPN». Список держите коротким, иначе модель начнёт вставлять эти слова там, где их не было.
Кодировка stdout у Python
Транскрипт приехал в чат вот таким:
услышал: ?? ??? ?????? ? ????? ?????? ??????
Python под Windows кодирует вывод в pipe локальной кодовой страницей, а не в UTF-8. Кириллица не пролезает.
Что меня тут подловило: мой собственный тест этого не показал. Я запускал скрипт из PowerShell, тот декодировал вывод кодовой страницей консоли, и всё выглядело нормально. А через pipe в Bun путь другой. Проверять надо ровно тем способом, которым это будет работать в бою, а не тем, которым удобно.
PYTHONUTF8=1 при запуске плюс reconfigure(encoding='utf-8') в скрипте.
CUDA под uv и ленивый генератор
Два бага, слипшихся в один симптом.
Первый: RuntimeError: Library cublas64_12.dll is not found. Мой скрипт искал CUDA-библиотеки через site.getsitepackages(), а тот под uv run возвращает эфемерный каталог сборки. Сами колёса nvidia-* лежат в контент-кеше uv и подключаются через sys.path. Сканировать надо sys.path и nvidia.__path__.
Второй интереснее. У меня был честный фолбэк на CPU, обёрнутый вокруг конструктора модели. Он не сработал ни разу. Причина в том, что model.transcribe() в faster-whisper возвращает ленивый генератор: вычисления начинаются при обходе сегментов, то есть уже после конструктора. Ошибка загрузки CUDA вылетала мимо всего моего try.
Фолбэк обязан оборачивать и обход генератора. Иначе это декорация.
Демон, умирающий вместе с ярлыком
Мост запускался через Start-Process -NoNewWindow, то есть в консоли родителя. Окно ярлыка закрывалось через шесть секунд, и мост получал сигнал закрытия вместе с ним. В логе три строки о успешном старте и тишина.
Для фонового процесса нужен -WindowStyle Hidden: своя консоль отвязывает его от родителя.
Отмена, которая оставляла сирот
Отмена задачи из чата сначала работала через taskkill /PID x /T /F. Замерил: из процессов дерева умирает один, двое остаются.
Причина в том, что к моменту отмены claude уже развернул рабочие процессы. Когда промежуточный родитель исчезает, они переподвешиваются к другому и выпадают из дерева. Обход по ParentProcessId их тоже не находит.
Пришлось убивать по маркеру: искать процессы, в чьей командной строке есть id сессии, и добирать их потомков. После этого стало killed 10, осталось ноль.
И маленькая история про измерения. Первая версия теста показывала двух выживших даже после починки. Оказалось, что считающий PowerShell получал маркер прямо в свою командную строку и попадал под собственный фильтр. Механизм был исправен; врал измеритель.
Что в итоге получилось
Голосовое из дороги превращается в задачу. Мост скачивает .oga, гоняет через Whisper, отбивает в чат «услышал: …» для проверки, и только потом работает. Эхо тут не украшение: один раз я убедился, что распознано не то, ещё до того как что-то поехало не туда.
Отмена сделана кнопкой под сообщением о прогрессе, и это оказалась самая нужная мелочь. Аналог Esc в CLI, но с одним важным свойством: контекст сохраняется. Сессия та же, следующее сообщение продолжает диалог. Проверял отдельным тестом: попросил запомнить кодовое слово, отменил задачу на десятом шаге, следующим сообщением спросил слово. Вспомнил.
Ради этого пришлось поменять момент сохранения сессии. Изначально id писался по событию done, и отмена первой задачи в чате обнуляла контекст: сессию просто не успевали записать. Теперь id забирается из первого же события.
Файлы приходят документами, а не текстом с путями. Разница принципиальная, когда результат работы надо переслать заказчику: путь C:...INSTRUCTION.md в мессенджере бесполезен, а документ форвардится в два тапа. Claude заканчивает ответ блоком с путями, мост его вырезает и отправляет файлы.
Конвенция намеренно явная, а не «выдерни пути из текста регуляркой». Пути в ответах упоминаются постоянно, и угадывание превратилось бы в рассылку случайных файлов.
Прогресс виден отдельным сообщением, которое правится по ходу: «работаю · шагов 7 · Bash: сборка». Финал приходит новым сообщением, потому что только новое даёт пуш на телефон. Правка старого не даёт.
Модель и глубину раздумий можно переключать из чата: /model, /effort, /new, /status. Мелочь, но именно её мне не хватало в первый же день.
Чего оно не умеет и где я схитрил
Только Windows. Логика моста кросс-платформенная, а вот отмена задач и лончеры написаны на PowerShell.
Одна задача на чат за раз, потому что --resume одной сессии двумя процессами недопустим.
Компьютер должен быть включён. Сообщения при этом не теряются: Telegram держит очередь и отдаёт накопленное после перезапуска, что меня один раз выручило.
И главное, о чём надо сказать прямо. Права по умолчанию стоят в bypassPermissions, то есть Claude работает автономно и ничего не спрашивает. Это не лень, а следствие конструкции: подтверждать запросы разрешений из Telegram негде, и задача просто встанет насмерть в ожидании ответа, которого никто не даст. Официальный плагин, кстати, это умеет: у него есть relay с инлайн-кнопками. Ещё один повод предпочесть его, если Channels вам доступны.
Отсюда честное предупреждение. Кто попал в allowlist, тот получает автономное выполнение кода на вашей машине с доступом к вашим файлам и ключам. Держите там только себя. В групповых чатах обязательно ограничивайте список участников, иначе задачи сможет ставить любой, кто в чате.
Меня до сих пор немного смущает эта развилка. Я выбрал автономность сознательно, потому что иначе вся затея не имеет смысла, но не могу сказать, что мне полностью нравится такой выбор.
Код
Всё выложено под MIT: github.com/by-sonic/claude-code-telegram-ru
Там мост, лончеры, транскрибер и шаблоны конфигов. Если Channels у вас работают, берите официальный плагин, он лучше. Этот мост нужен ровно тем, у кого тумблер выключен и включить его некому.
Отдельно скажу про способ работы, потому что вопрос всё равно возникнет. Проект я собирал вместе с Claude Code, и эту статью тоже писал с ним. Мне это кажется уместным: статья про инструмент, собранный этим же инструментом. Все цифры, ошибки и цитаты в тексте настоящие, из реальной отладки, включая тот вежливый ответ про обрывки мысли.
Если у вас похожая тишина от бота при живом MCP-сервере, начните с теста через fakechat. Сэкономите себе вечер, а мне будет приятно, что не зря его потратил.
Автор: babin2002


