Архитектура

aipager это программа на asyncio из одного процесса, которая работает в фоне. Она соединяет три мира: твой чат в Telegram, процессы Claude Code, которые ты запускаешь под dtach, и систему hooks Claude Code. Всё проходит через один цикл событий: никаких пулов воркеров, никакой базы данных, никаких очередей между процессами.

Исходник на GitHub ↗Эта страница переведена с английской; если они расходятся, верна английская версия. English

aipager это программа на asyncio из одного процесса, которая работает в фоне. Она соединяет три мира: твой чат в Telegram, процессы Claude Code, которые ты запускаешь под dtach, и систему hooks Claude Code. Всё проходит через один цикл событий: никаких пулов воркеров, никакой базы данных, никаких очередей между процессами.

Схема частей#

Loading diagram…

Всё на схеме работает на этом компьютере, кроме Telegram. aipager связывается ещё с несколькими серверами: с туннелем Cloudflare для Mini App (и один раз с GitHub, ради cloudflared), с telegram.org ради скрипта Mini App, с сервером проверки версий, когда ты просишь обновление, с сервером, откуда скачивается голосовая модель, когда впервые пользуешься голосовыми, и с адресом для отчётов о проблемах, когда ты отправляешь отчёт. Все они перечислены в разделе Модель безопасности → сетевые соединения.

Как устроен процесс#

Один вызов asyncio.run в aipager/cli/daemon.py ведёт всё. Внутри:

  • TelegramBot (aipager/bot/): владеет Application из python-telegram-bot. Получает обновления от Telegram, раздаёт их обработчикам сообщений, кнопок и голосовых и держит в актуальном виде рабочие карточки, закреплённое сообщение и ответы.
  • HookReceiver (aipager/dtach/hook_receiver.py): открывает Unix datagram-сокет в $XDG_RUNTIME_DIR/aipager.sock, читает JSON, который присылают aipager-hook и aipager-statusline, и распределяет работу по его hook_event_name (или type, для собственных сообщений aipager; см. события hooks).
  • SessionMonitor (aipager/session_monitor.py): просыпается каждые 2 с, чтобы просмотреть /tmp/claude-dtach-*.sock, сверить найденные сессии со списком сессий и запустить сторожей: рабочая карточка, которая перестала двигаться, сообщение, которое Claude Code так и не принял, запрос разрешения без новостей 5 минут, который по сохранённой переписке уже закончен (AIPAGER_INTERACTIVE_TIMEOUT), задача, конец которой пропущен.
  • SessionRegistry (aipager/state.py): объекты TrackedSession по именам. Сохраняются в ~/.claude/aipager-sessions.json (атомарно) при выключении и каждый раз, когда меняется что-то, что должно пережить перезапуск.
  • Сервер Mini App (aipager/miniapp/server.py): приложение aiohttp на 127.0.0.1 с портом MINIAPP_PORT (по умолчанию 8765), доступное Telegram через управляемый туннель Cloudflare, пока оно включено (по умолчанию так). Отдаёт панель /app.

Всё это работает как задачи в одном цикле. Работа, которая бы его блокировала, идёт в отдельных потоках: превращение голосовых в текст, сборка и отправка отчёта о проблеме, проверка обновлений и подготовка перезапуска, скачивание скрипта Mini App и cloudflared и проверка входа при старте.

Порядок запуска#

Из aipager/cli/daemon.py:

  1. Один раз перенести старую конфигурацию (config.env, team.yaml) в aipager.yaml и убрать старые файлы в сторону.
  2. Отказаться запускаться, если на сокете отвечает другой aipager, затем взять блокировку aipager (~/.local/share/aipager/daemon.lock).
  3. Проверить токен бота в Telegram (getMe).
  4. Привести в порядок файлы самого Claude Code: добавить в ~/.claude/settings.json недостающие hooks aipager и строку состояния и ответить на вопросы первого запуска, на которые человек в Telegram ответить не может (см. безопасность).
  5. SessionRegistry.load(): читает ~/.claude/aipager-sessions.json. Если файла нет или он не читается, список начинается пустым.
  6. Определить адрес Mini App (твой собственный URL или Tailscale), затем bot.start(): начинает опрашивать Telegram.
  7. HookReceiver.start(): удаляет старый файл сокета, привязывается к сокету, слушает.
  8. bot.recover_sessions(): возвращает карточки и запросы сессий, которые работали или ждали, когда aipager остановился (см. команды → ответы).
  9. SessionMonitor.start(): начинает проход каждые 2 с.
  10. Запускается сервер Mini App, потом туннель (если не задан твой собственный URL), и публикуются кнопки 📱 App.
  11. aipager ждёт сигналов: SIGINT или SIGTERM его останавливают, SIGUSR1 перезагружает чаты и правила (группы → перезагрузка без перезапуска).

Порядок выключения#

  1. SIGINT или SIGTERM выставляет событие stop.
  2. Идущий /update получает короткую отсрочку, потом его установщик останавливается (см. команды → обновление).
  3. registry.save().
  4. Останавливается туннель, потом сервер Mini App.
  5. session_monitor.stop(), hook_receiver.stop() (закрывает сокет и удаляет его файл).
  6. bot.stop(): отменяет анимации и останавливает Application.
  7. Ещё раз registry.save(), для карточки, отправка которой ещё шла.

Перезапуск из Telegram рассчитывает на то, что весь этот путь пройдёт до конца, прежде чем замена привяжется к сокету. Что при этом видно тебе, описано в команды бота → перезапуск.

Где лежат файлы и сокеты#

ПутьНазначениеКто пишет
$XDG_RUNTIME_DIR/aipager.sockUnix datagram-сокет для событий hooks (/tmp/aipager.sock без $XDG_RUNTIME_DIR; $AIPAGER_SOCKET_PATH заменяет оба)aipager (привязывается)
aipager-reply-<id>.sock (рядом с управляющим сокетом)Короткоживущий сокет ответа для каждого запроса разрешения, которого ждёт hookaipager-hook
aipager-flood-mute.json, aipager-flood-backoff.json (рядом с управляющим сокетом)Текущее состояние лимитов Telegram, для aipager status и doctoraipager
aipager-modelswitch-<name>.json (рядом с управляющим сокетом)Отмечает смену модели, которую ввёл aipager, на 30 с (hooks)aipager (пишет), aipager-hook (забирает)
/tmp/claude-dtach-<name>.sockУправляющий сокет dtach для каждой сессииdtach
/tmp/claude-status-<name>.jsonДанные строки состояния каждой сессииaipager-statusline
/tmp/claude-notes-<name>/По одной заметке о разрешениях на каждое ещё не забранное сообщение из Telegram и отметка turn-open, которую hook держит, пока идёт задача (сообщение, которое присоединяется к задаче, может только сузить её правила)aipager (заметки, пишет), aipager-hook (расходует заметки; пишет turn-open, а стирают её оба)
/tmp/claude-policy-<name>.jsonОпорный снимок разрешений для идущей задачиaipager-hook (пишет, когда забирает сообщение), читает проверка PreToolUse
/tmp/claude-policy-.floor-<uid>.jsonНижний порог безопасности: встроенный список плюс твой раздел safety:, для запасных путей hookaipager
/tmp/claude-reply-<name>.txtСообщение, на которое ты отвечаешь, когда это более старое сообщениеaipager
/tmp/aipager-files/<chat>/Файлы и голосовые, присланные из Telegram, по одной закрытой папке на каждый чат (0700, файлы 0600)aipager
/tmp/aipager.logВывод перезапуска самим aipager, когда службы нетaipager
~/.claude/aipager-sessions.jsonДолговременное состояние списка сессийaipager
~/.claude/aipager-audit.jsonlЖурнал Разрешить / Отклонить / ответовaipager (только дописывает)
~/.claude/aipager-flood-state.jsonИстория лимитов Telegram для каждого чата, сохраняется между перезапусками (решение проблем)aipager
~/.claude/aipager-tunnel.json, aipager-cloudflared.logТуннель Mini App, который держит aipager: его процесс, локальные порты и хеш его адреса (никогда не сам адрес), чтобы следующий aipager мог подхватить его после планового перезапуска (права 600); предупреждения cloudflaredaipager; запись также удаляют сторож оставленного туннеля и aipager uninstall
~/.claude/aipager-pending-users.jsonЛюди, которые обратились к боту в группе, которую он не обслуживает, или которые не участники группы, которую он обслуживает, чтобы aipager config мог их добавить (права 0600, целиком перезаписывается при каждой записи, последние 200 человек за 30 дней)aipager (пишет), aipager config (читает)
~/aipager-users/<label>/Корневые папки участников по умолчанию (права 0700), где участники с user и read_only запускают свои сессии; aipager uninstall их сохраняетaipager (создаёт при первом /new или первой «Новой сессии» в Mini App у участника)
~/.claude/settings.jsonКонфигурация hooks Claude Codeaipager config / aipager setup; при каждом запуске aipager добавляет недостающие hooks или строку состояния aipager и ставит skipDangerousModePermissionPrompt (безопасность)
~/.claude/settings.json.bak.*Резервные копии перед каждой перезаписьюaipager config, aipager setup, aipager uninstall
~/.claude.jsonСобственный файл Claude Code: aipager отмечает папку, в которой запускается сессия, как доверенную прямо перед запуском (домашнюю папку тоже, но никогда / или папку выше домашней), чтобы вопрос о доверии Claude Code не останавливал сессию, запущенную из Telegram. При запуске aipager ничего не отмечается. Никогда не перезаписывается, если его нельзя прочитать целиком или работающий Claude успел его изменитьaipager
~/.claude/.credentials.json → .credentials.json.staleФайл входа Claude Code, отодвигается в сторону, когда сессия запускается с заданным CLAUDE_CODE_OAUTH_TOKEN, а в файле только истёкший или пустой вход (иначе он взял бы верх над токеном); чтобы отменить, верни его на местоaipager
~/.config/aipager/aipager.yamlТокен бота, чаты, участники + роли, настройки Mini Appaipager config (права 600); aipager переносит запись группы, когда Telegram делает её супергруппой, и сохраняет копию aipager.yaml.bak.<time>, когда переносит туда старые настройки Mini App
~/.config/aipager/policy.yamlПравила для каждой роли + дополнения к порогу безопасноститы (проверяется через aipager policy validate); шаблон с комментариями пишется один раз при переносе старой установки
~/.config/aipager/preferences.jsonВыбор в /settings для каждого чата и каждой сессииaipager
~/.config/aipager/daemon.envУчётные данные Claude для запускаемых сессийты / aipager service install / aipager doctor --fix (права 600)
~/.config/aipager/config.env, team.yaml → *.bak.<time>, *.retired.<time>Конфигурация старой установки: копируется, а потом переименовывается в сторону, когда её уже содержит aipager.yamlaipager, aipager config, aipager setup
~/.config/aipager/keyboard.jsonНеобязательные изменения клавиатурыты
~/.local/share/aipager/sessions/По папке на сессию с её SESSION.md (кто может к ней обращаться и правила), пересоздаётся при каждом запускеaipager
~/.local/share/aipager/webapp-sdk/, cloudflared/Скрипт Mini App от Telegram и проверенная программа cloudflaredaipager
~/.local/share/aipager/daemon.lock, update.lock, update-restart.jsonТолько один aipager одновременно; только одно обновление одновременно; обновление, о котором надо сообщить после перезапускаaipager
~/.local/share/aipager/install.json, running.jsonКогда aipager впервые запустился здесь; работает ли он сейчас (чтобы отличить падение от нормальной остановки)aipager
~/.local/state/aipager/reports.jsonОтчёты о проблемах: записанные отпечатки ошибок, их количество и состояние предложений (права 600); см. отчёты о проблемахaipager
~/.local/state/aipager/report-sends.jsonОтчёты о проблемах, отправленные сегодня (дневной лимит)aipager, aipager report
~/.local/state/aipager/languages.jsonСостояние языков (права 600): кто уже пользовался aipager до появления языков, последний язык Telegram, замеченный у каждого человека, ушли ли разовое предложение языка и строка первого контакта, и отпечаток каждого описания бота, которое поставил aipager; см. Языкиaipager
~/.config/systemd/user/aipager.service (Linux), ~/Library/LaunchAgents/com.aipager.daemon.plist (macOS) и копия .bak.<time> того, что заменяет установка (на Linux только если он отличается)Фоновая службаaipager service install
~/Library/Logs/aipager.log (macOS)Лог службы launchd, его читает aipager logslaunchd, для службы

Кроме этого, aipager создаёт новую папку сессии, когда ты её просишь (Новая папка в /new или в Mini App), а установщики и голосовая модель пишут свои файлы, когда ты обновляешься или пользуешься голосовыми. Всё это лежит в твоей домашней папке, в /tmp или рядом с управляющим сокетом. aipager никогда не повышает свои права; см. безопасность.

Изолированный экземпляр (для тестов)#

Задай AIPAGER_INSTANCE_DIR как абсолютную папку, которая принадлежит тебе, и каждый общий рабочий файл из списка выше переедет в неё: управляющий сокет (а рядом с ним сокеты ответов, файлы сигналов лимитов и отметка о смене модели), сокеты claude-dtach-*, файлы claude-policy-*, claude-notes-*, claude-status-* и claude-reply-*, файл порога, папка загрузок aipager-files и лог перезапуска самим aipager. Сессии, которые запускает этот экземпляр, наследуют настройку, поэтому их hooks говорят только с этим экземпляром, а правила безопасности защищают перенесённые файлы так же. Экземпляр не читает .env рабочей копии репозитория.

Файлы в твоей домашней папке не переезжают, поэтому направь и HOME на отдельную папку. aipager start отказывается работать, если задана папка экземпляра, а HOME это твоя настоящая домашняя папка, если папка не является абсолютным путём к папке, которая принадлежит тебе, или если она слишком длинная для пути сокета.

/update и /restart не изолированы: они действуют на установленный пакет aipager и службу aipager.service, поэтому никогда не отправляй их тестовому экземпляру.

AIPAGER_TELEGRAM_API_BASE (например, http://127.0.0.1:41234) отправляет каждый запрос к Bot API и каждую загрузку файла на этот адрес вместо https://api.telegram.org. Это должен быть этот компьютер (localhost или адрес loopback), если только не задано AIPAGER_TELEGRAM_API_ALLOW_REMOTE=1, чтобы опечатка не могла отправить токен бота куда-то ещё.

Обе настройки нужны, чтобы сквозные тесты могли запускать второй aipager против локальной замены Telegram рядом с твоим настоящим. Без них каждый путь и каждый адрес такие, как в таблице выше.

Почему dtach#

dtach даёт каждой сессии Claude Code настоящий PTY, не привязывая его к терминалу, который должен оставаться открытым. aipager запускает сессию через dtach -n и вводит в неё через dtach -p (сообщения, Escape, клавиши для ответа в диалоге); экран он никогда не читает, потому что о происходящем говорят hooks и история переписки. Подключиться к сессии напрямую можно из любого терминала командой aipager session <name>. Такое подключение следует за сессией через перезапуск из Telegram (/mode на режим «Полный доступ» или обратно, /restart, Перезапустить): когда сокет сессии пропадает, оно ждёт до 15 с, пока вернётся тот же сокет, и подключается снова само. Сессия, которая действительно завершилась, после ожидания возвращает тебя в командную строку (shell) (Ctrl-C прекращает ожидание раньше). Подключение работает с выключенной клавишей отсоединения dtach, поэтому Ctrl-\ уходит в Claude; чтобы уйти, закрой терминал или панель tmux, и сессия продолжит работать.

Итог: Claude Code работает так же, как при наборе в терминале, но этот терминал может появляться и пропадать, не мешая идущей сессии. aipager находит сессии, просматривая /tmp/claude-dtach-*.sock на каждом проходе монитора раз в 2 с. Найденная сессия принадлежит чату, для которого было создано её имя, а сессия, запущенная в терминале, на установке с личным чатом и группой принадлежит личному чату владельца.

См. также#