33 KiB
Delta Chat Bot: deltabot
⚠️ Важно: как мы работаем
- Я работаю на локальном Mac (
/Users/alexabudaev/Documents/Zed/), не на сервере. - Все изменения — только локально. После завершения работы —
git push origin main. - Репозиторий:
https://git.budaev.org/alexabudaev/delta-chat-bot - Авторизация: HTTPS + токен (credentials в
~/.config/git/forgejo-credentials) - Сервер: Ubuntu 24.04, бот под
alexabudaev, Postfix/Dovecot подbot.
Авто-деплой на сервер
На сервере настроен systemd timer — каждые 5 минут проверяет обновления в main и перезапускает бота:
sudo systemctl status deltabot-auto-update.timer
Принудительный деплой (если надо прямо сейчас):
ssh alexabudaev@10.0.0.1 "cd ~/delta-bot && git pull origin main && sudo systemctl restart deltabot"
Прокси (PHP)
После изменений в php-proxy/ — scp вручную (пока не в git на сервере):
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/*.php alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/.htaccess alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/channels.json alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
Архитектура
deltabot.py
└── deltachat_rpc_client (Bot, DeltaChat, Rpc)
└── deltachat-rpc-server (Rust Core)
Структура проекта
deltabot.py— основной скрипт бота (JSON-RPC).AGENTS.MD— документация проекта.- Конфиги и БД:
~/.config/deltabot/(subscribers, channels, pending, bridges) - Аккаунты DC:
~/delta-bot/accounts/
Развёртывание
# После изменений — просто пуш:
git push origin main
# На сервере авто-деплой каждые 5 мин. Принудительно:
ssh alexabudaev@10.0.0.1 "cd ~/delta-bot && git pull origin main && sudo systemctl restart deltabot"
# Логи:
ssh alexabudaev@10.0.0.1 "sudo journalctl -u deltabot -f | grep 'TG Broadcast'"
Natural Language → Commands
Любое сообщение, не начинающееся с /, проходит через parse_natural_language() (строка 101):
| Фраза | Команда |
|---|---|
погода/weather/прогноз [город] [на N дней/неделю] |
/weather ... |
дай/мосты/bridge/tor [obfs4/vanilla/ipv6] |
/bridges ... |
статус/status/состояние |
/status |
помощь/help/команды/что умеешь |
/help |
подпишись/подписаться/subscribe |
/subscribe |
отпишись/отписаться/unsubscribe |
/unsubscribe |
qr/qrcode/кьюар |
/qr |
Переменные окружения (.env)
Секреты вынесены в ~/.config/deltabot/.env:
GMAIL_EMAIL=dtorbot@gmail.com
GMAIL_APP_PASSWORD=eciu llvw sjjz ygbb
CLOUDFLARE_ACCOUNT_ID=7fb91c91800780ae7dcdf5d47cdf9a1d
CLOUDFLARE_API_TOKEN=<token>
OPENROUTER_API_KEY=sk-or-...
CALDAV_URL=https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/
CALDAV_USER=alex@budaev.org
CALDAV_PASSWORD=<пароль>
Права: chmod 600 ~/.config/deltabot/.env. systemd подхватывает через EnvironmentFile= в deltabot.service.
Обязательные (без них не работают мосты): GMAIL_EMAIL, GMAIL_APP_PASSWORD. Для AI: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN. OPENROUTER_API_KEY — опционально (fallback). Для CalDAV: CALDAV_USER, CALDAV_PASSWORD (URL зашит как дефолт).
Команды бота
/status— состояние сервера/weather [город] [дни]— погода (3/7/10 дней)/bridges [obfs4|vanilla|ipv6]— Tor мосты/save [путь]— сохранить прикреплённый файл/subscribe//unsubscribe— ежедневный отчёт (09:00 IRKT)/channels list|add|remove|invite|description|image— управление broadcast каналами/telegram <username> [N]— посты из TG канала/cal today|week|list|add|delete— управление CalDAV-календарём/note add|list|delete— заметки (per-chat)/rate [валюта...]— курс валют ЦБ РФ/ip [адрес]— внешний IP сервера или информация об адресе/dns <домен>— DNS-запрос/monitor add|list|remove|check— мониторинг сайтов/ai summary— краткое резюме истории AI-чата- Голосовые сообщения → автоматическая транскрипция (Whisper via Cloudflare)
/qr— QR код для добавления бота/join https://i.delta.chat/#...— secure join/addcontact <email|*@domain>— добавить email или домен в белый список (только для верифицированных)/contacts list|remove— просмотр и управление белым списком/help— справка
⚠️ /help и echo (ответ на не-команду) — один HELP_TEXT. Определён как константа в начале файла. При добавлении/изменении команд править только HELP_TEXT. Echo не отстанет от help.
PHP Proxy (кэширующий прокси для tg.i-c-a.su)
Архитектура:
tg.i-c-a.su (10-17s) → proxy.budaev.org (rss.php + media.php + кэш) → бот (<0.1s)
Исходники: php-proxy/ — заливаются на proxy.budaev.org (хостинг с ISPManager).
Два PHP-скрипта (без cron — кэш наполняется лениво):
| Файл | Роль |
|---|---|
rss.php |
GET ?channel=CHANNEL&limit=N: отдаёт RSS из кэша (TTL 600с + stagger), при промахе проксирует с tg.i-c-a.su |
media.php |
GET ?url=...: отдаёт картинку из кэша (TTL 24ч), при промахе скачивает и кэширует |
rss.php (оптимизирован 02.06.2026):
- Pre-rewritten cache: XML сохраняется в кэш уже с переписанными
<enclosure url>(на media.php). При cache hit —readfile()без DOMDocument. - Staggered TTL:
600 + crc32(channel) % 300секунд, чтобы каналы не истекали одновременно. - Combined DOMDocument: fetch + rewrite в одном проходе (раньше было два DOMDocument на miss).
- Empty feed вместо 502: при ошибке tg.i-c-a.su и отсутствии кэша возвращается пустой RSS, чтобы бот не падал на fallback и не получал сырые enclosure URL.
media.php (оптимизирован 02.06.2026):
- Probabilistic cleanup:
maybeCleanOldCache()— при ~2% запросов удаляет файлы старше 24ч. Без cron. - Cache-Control унифицирован: и HIT и MISS отдают
max-age=86400(было 604800 на MISS — неконсистентно с TTL).
channels.json — список каналов (для информации, работа не зависит от него).
Новый канал: первый poll медленный (10-30с, rss.php проксирует), со второго — мгновенно.
Телеграм с хостинга недоступен (блокировка), поэтому rss.php ходит через tg.i-c-a.su.
IP restriction
Доступ к proxy.budaev.org разрешён только с IP бота (90.188.48.201). Реализовано через .htaccess:
RewriteCond %{REMOTE_ADDR} !^90\.188\.48\.201$
RewriteRule ^ - [F,L]
Остальные IP получают 403 Forbidden на уровне Apache (до PHP не доходит).
Empty feed вместо 502
При ошибке tg.i-c-a.su, если нет даже старого кэша, rss.php возвращает пустой RSS <rss><channel><title>...</title></channel></rss> с HTTP 200, вместо 502. Это не даёт боту упасть на fallback (tg.i-c-a.su напрямую), который возвращает сырые enclosure URL без перезаписи на media.php.
Upstream timeout
rss.php ждёт ответ от tg.i-c-a.su до 30 секунд (было 20, увеличено 24.05.2026 из-за тяжёлых каналов типа gremtelegram, которые не влезали в 20с).
Telegram Channels (Broadcast)
- Прокси (основной):
https://proxy.budaev.org/rss(TG_RSS_PROXY) - Прокси (fallback):
https://tg.i-c-a.su/rss(TG_RSS_FALLBACK) — если proxy.budaev.org недоступен - Бот пробует сначала основной, при ошибках — fallback (
get_telegram_feed(), строки 189-238) - Polling каждые 90 секунд,
limit=10, обработка всех новых постов (batch) - Кэш RSS: 600с TTL. Из ~7 poll-ов 1 медленный (10-30с для тяжёлых каналов), остальные — мгновенные (<0.1с)
- При пропуске канала (ошибка RSS) —
logger.warningс причиной и временем запроса - Для каждого канала логируется какой прокси сработал и сколько времени занял запрос:
TG Broadcast: Fetched gremtelegram (20.6s, 4 posts) - Неудачные скачивания картинок —
logger.debug(раньше был warning, засорял логи) - Описание канала устанавливается из RSS при создании. Картинка — вручную через
/channels image
Enclosure-картинки
Прокси (tg.i-c-a.su) добавляет <enclosure url="..." type="image/jpeg" length="..."/> в RSS для постов с медиа. Бот:
- Читает первый
<enclosure>из RSS - Если
typeначинается сimage/— скачивает эту картинку - Прикрепляет к тексту поста (поле
fileвsend_msg) - Удаляет временный файл после отправки
- Если скачать не удалось — пост уходит без картинки
HTML → plain text
Delta Chat не рендерит Markdown. Из HTML вырезаются все теги:
| Тег в RSS | Результат |
|---|---|
<br> |
перенос строки |
<a href="url">text</a> |
сохраняется только text, URL → сноска |
<a href="*max.ru*">...</a> |
URL не попадает в сноску, текст сохраняется |
| Сноска | ---\nurl1\nurl2 |
<b>, <i>, <blockquote> и т.д. |
удаляются |
<tg-emoji> / <a><img> |
удаляются |
| HTML entities | html.unescape() |
<title> |
fallback, если <description> пустой |
Код: telegram_poll_worker(). Ссылки через callback collect_url() — max.ru исключаются.
CalDAV — Управление календарём
Статус: работает
Сервер: Baikal на baikal.budaev.org
Календарь: https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/
Зависимости: только requests (уже в зависимостях), uuid, xml.etree.ElementTree (stdlib)
Команды
| Команда | Действие |
|---|---|
/cal today |
События на сегодня (UTC → Иркутск) |
/cal week |
События на 7 дней |
/cal list [N] |
События на N дней (по умолч. 30), с короткими ID |
/cal add <дата> <время> <название> [описание] |
Добавить событие |
/cal delete <ID> |
Удалить событие по коротому ID (первые 8 символов UID) |
Форматы даты в /cal add: сегодня, завтра, ДД.ММ, ДД.ММ.ГГГГ
Примеры:
/cal add сегодня 15:00 Встреча с командой
/cal add завтра 10:30 Визит врача онлайн
/cal add 20.06 09:00 Конференция описание здесь
Архитектура
handle_message() → /cal subcmd
├── today/week → cal_list_events(days) → REPORT (CalDAV) → форматирование
├── list [N] → cal_list_events(N) → REPORT (CalDAV) → форматирование + short ID
├── add → _parse_cal_datetime() → cal_add_event() → PUT (CalDAV)
└── delete <ID> → cal_list_events(365) → поиск по UID-префиксу → cal_delete_event() → DELETE
HTTP-методы CalDAV
| Операция | Метод | Тело |
|---|---|---|
| Получение событий | REPORT + Depth: 1 |
XML calendar-query с time-range |
| Создание события | PUT на <uid>.ics |
iCalendar (VCALENDAR/VEVENT) |
| Удаление события | DELETE на <uid>.ics |
— |
Часовой пояс событий: Asia/Irkutsk (UTC+8). При создании пишется TZID=Asia/Irkutsk в DTSTART/DTEND.
Переменные окружения
CALDAV_URL=https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/
CALDAV_USER=alex@budaev.org
CALDAV_PASSWORD=<пароль_baikal>
CALDAV_URL имеет дефолтное значение в коде — можно не задавать, если URL не меняется.
Secure join
Единственный способ добавить контакт (create_contact RPC сломан на core 2.49.0). Бот обрабатывает ссылку https://i.delta.chat/#... в фоновом потоке через account._rpc.secure_join(). Чтобы handshake завершился, получатель должен ответить (принять приглашение). Пока ответа нет — key is missing при отправке.
Mail сервер (dc.budaev.org)
Системный пользователь bot
- Postfix: приём на 25 (postscreen), submission на 587 (STARTTLS + SASL), доставка напрямую на MX (relayhost выключен).
- Dovecot: SASL через PAM (пользователь
bot), maildir в~/Maildir. - Сертификаты: Let's Encrypt,
/etc/letsencrypt/live/dc.budaev.org/. - sasl_passwd:
[dc.budaev.org]:587 bot:ПАРОЛЬ(usernamebot, неbot@...). - Важно:
relayhostне ставить — loop.smtp_tls_wrappermode/smtp_use_tlsне использовать.
Виртуальные ящики (Dovecot LMTP + passwd-file)
Для дополнительных адресов (например, macky@dc.budaev.org) используется Dovecot LMTP + passwd-file.
Postfix (main.cf):
transport_maps = hash:/etc/postfix/transport
Transport (/etc/postfix/transport):
macky@dc.budaev.org lmtp:unix:private/dovecot-lmtp
Dovecot LMTP (/etc/dovecot/conf.d/20-lmtp.conf):
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
group = postfix
mode = 0600
user = postfix
}
}
⚠️ На Ubuntu пользователь postfix, не _postfix (как на macOS).
passwd-file auth (/etc/dovecot/conf.d/auth-passwdfile.conf.ext):
passdb {
driver = passwd-file
args = scheme=crypt username_format=%u /etc/dovecot/users
}
userdb {
driver = passwd-file
args = username_format=%u /etc/dovecot/users
default_fields = uid=5000 gid=5000 home=/var/mail/vhosts/dc.budaev.org/%n
}
Users (/etc/dovecot/users, формат passwd-file):
macky@dc.budaev.org:{SHA512-CRYPT}$6$...:5000:5000::/var/mail/vhosts/dc.budaev.org/macky:/usr/sbin/nologin
Порядок настройки для нового виртуального ящика:
- Установить
dovecot-lmtpd(отдельный пакет на Ubuntu) - Создать конфиги LMTP и auth-passwdfile
- Раскомментировать
!include auth-passwdfile.conf.extв/etc/dovecot/conf.d/10-auth.conf - Создать запись в
/etc/dovecot/users(хэш черезdoveadm pw -s SHA512-CRYPT) - Создать Maildir с uid/gid из userdb:
mkdir -p /var/mail/vhosts/dc.budaev.org/macky/Maildir/{new,cur,tmp}; chown -R 5000:5000 - Добавить transport в Postfix:
echo 'macky@dc.budaev.org lmtp:unix:private/dovecot-lmtp' >> /etc/postfix/transport && postmap /etc/postfix/transport - Перезапустить dovecot + postfix
Известные грабли mail
- relayhost — НЕ СТАВИТЬ. Бот аутентифицирован на submission:587, Postfix сам доставляет на внешние MX.
- smtp_tls_wrappermode / smtp_use_tls — ломают TLS. Не использовать.
bot@dc.budaev.orgvsbot— в sasl_passwd username должен бытьbot, неbot@dc.budaev.org.- key_id в БД — после DELETE keypairs.id autoincrement не переиспользуется. Убедись что key_id в config совпадает с реальным id в keypairs.
- Spamhaus PBL — IP 90.188.48.201 в списке, iCloud/Gmail могут отклонять почту пока PTR не заработает.
- dovecot-lmtpd — обязательный пакет на Ubuntu, без него LMTP сокет не создаётся.
- Group
postfixvs_postfix— на Ubuntu пользовательpostfix, на macOS_postfix. - Permission denied autocreate Maildir — виртуальный ящик не может создать Maildir сам, нужно создать вручную с uid/gid, совпадающим с
default_fieldsв userdb. local_recipient_maps =— обязательно очистить (Postfix иначе rejected для виртуальных), доставка через transport_maps.
DKIM
OpenDKIM milter (Mode sv) верифицирует входящие, но не вызывается для исходящих (submission). Решение не deployed. Документировано в AGENTS.MD.bak — dkimpy pipe after-queue.
Bridges (Tor мосты)
Запрос через Gmail-посредник. Кэш 6ч, лимит 5 мостов на тип. Баг дубликатов (16.05.2026) починен: добавлен _tor_lock, сохранение delivered в файл, проверка в цикле.
Известные проблемы
- "key is missing" при отправке в новый чат — secure join handshake не завершён.
- Описание канала —
set_chat_descriptionтребует core v2.50.0 (не released). - Аватарка канала —
set_image()может не работать на core v2.49.0. - Greylisting на mail.budaev.org —
451-Greylisted, потом421 Too many connections. - Spamhaus PBL — PTR для 90.188.48.201 ещё не разрезолвился.
- tg.i-c-a.su ненадёжен — частые таймауты (17s+), per-channel баны, 15 RPM. PHP-прокси на budaev.org смягчает (кэш + fallback).
- Unbound на сервере — медленный резолвинг (122ms), рекомендуется форвардинг на 1.1.1.1.
Планы на оптимизацию
- Поддержка аватарок каналов (
chat.set_image()). - Очистка старых аккаунтов (
~/.local/share/deltachat/accounts/). - Автообновление LE сертификата (certbot renew + хуки).
- Уменьшить MDN (уведомления о прочтении).
Выполненные оптимизации
02.06.2026 — Proxy + bot
rss.php: pre-rewritten cache (cache hit =readfile(), без DOMDocument), staggered TTL (600 + crc32%300), combined DOMDocument.rss.php: пустой RSS вместо 502 при ошибке tg.i-c-a.su — бот не падает на fallback..htaccess: убрано правило прямого кэша (мешало: нет проверки TTL, отдавало устаревшие файлы).media.php:maybeCleanOldCache()— вероятностная очистка (~2% запросов) файлов старше 24ч.deltabot.py:TG_POLL_INTERVAL90→180с.channels.json: синхронизирован с реальным списком (6 каналов: markettwits, raiznews, droidergram, gremtelegram, postnauka, kartiny2).
13.06.2026 — Security fixes
deltabot.pysmtp_send(): убраныcontext.check_hostname = Falseиcontext.verify_mode = ssl.CERT_NONE— TLS к Gmail теперь с полной проверкой сертификата. Ранее Gmail app password передавался через непроверенное соединение.deltabot.py/save: ограничение размера файла до 50 МБ (os.path.getsize) до base64-кодирования — защита от DoS через pending_save.json.deltabot.py/join: проверка URL заменена с"i.delta.chat" in valueнаre.match(r'https://i\.delta\.chat/#', value)— устраняет обход подстрокой.deltabot.py/saveи/channels image:file_blobвалидируется черезos.path.realpath+ проверка что результирующий путь остаётся внутриACCOUNTS_DIR— защита от path traversal.
13.06.2026 — Изоляция бота
deltabot.py: добавлена проверка отправителя в началеhandle_message. Сообщения от неизвестных email игнорируются без ответа.deltabot.py:is_contact_allowed()— проверяетisVerified(JSON-RPC), затемTRUSTED_DOMAINS(zашиты в коде:budaev.org,dc.budaev.org), затемallowed_contacts.json.deltabot.py:_matches_allowlist()— поддержка масок*@domain.deltabot.py: команды/addcontactи/contacts list|remove— управление белым списком; изменения доступны только верифицированным контактам.
04.06.2026 — Poll interval decreased
deltabot.py:TG_POLL_INTERVAL180→90с — интервал уменьшен для устранения пропусков в часто обновляемых каналах.deltabot.py: Мульти-енклозур откачен (один пост — одна картинка, как раньше).
26.05.2026 — strip_markdown
deltabot.py: добавленаstrip_markdown()— удаляет**,*,`,[]()(→ сноски),#, списки, цитаты,---из AI-ответов перед отправкой.
Изоляция бота (безопасность)
Статус: реализовано (13.06.2026)
Бот отвечает только контактам из белого списка. Все остальные сообщения молча игнорируются (logger.info) — бот не отвечает и не подтверждает получение.
Логика проверки (is_contact_allowed)
Для каждого входящего сообщения в самом начале handle_message:
is_contact_allowed(account, from_id)
1. account._rpc.get_contact(account.id, from_id)
2. contact['isVerified'] == True → разрешить (SecureJoin/QR)
3. contact['address'] in TRUSTED_DOMAINS → разрешить (домен в коде)
4. contact['address'] in allowed_contacts.json → разрешить (явный список)
5. иначе → игнорировать
Доверенные домены (зашиты в коде)
TRUSTED_DOMAINS = {"budaev.org", "dc.budaev.org"}
Любой адрес *@budaev.org или *@dc.budaev.org проходит без SecureJoin.
Белый список (allowed_contacts.json)
Файл: ~/.config/deltabot/allowed_contacts.json
Формат: ["user@example.com", "*@otherdomain.org"]
Поддерживаются:
- Точный email:
user@example.com - Доменная маска:
*@example.com
Управление белым списком
| Команда | Кто может | Действие |
|---|---|---|
/addcontact user@example.com |
верифицированный контакт | добавить email |
/addcontact *@domain.org |
верифицированный контакт | добавить домен-маску |
/contacts list |
любой разрешённый | показать список |
/contacts remove user@example.com |
верифицированный контакт | удалить из списка |
Право на изменение белого списка — только у контактов с isVerified = True (добавленных через SecureJoin/QR).
Известные ограничения
isVerifiedв JSON-RPC требует двусторонней верификации. После/joinбот верифицирует пользователя, но самisVerifiedможет остатьсяFalseдо завершения handshake. В таком случае пользователь должен находиться вTRUSTED_DOMAINSилиallowed_contacts.json.- При
/joinможет создаваться новый неверифицированный контакт — старый верифицированный остаётся отдельно. Workaround: использоватьTRUSTED_DOMAINSдля своих доменов.
Заметки (/note)
Файл: ~/.config/deltabot/notes.json
Формат: {chat_id: [{id, text, created}]}
ID — автоинкремент в рамках каждого чата.
| Команда | Действие |
|---|---|
/note add <текст> |
Добавить заметку |
/note list |
Список заметок чата |
/note delete <N> |
Удалить по ID |
Курс валют (/rate)
API: ЦБ РФ https://www.cbr.ru/scripts/XML_daily.asp (XML, без ключа)
Кэш: в памяти, TTL 1 час (_rates_cache dict).
| Команда | Действие |
|---|---|
/rate |
USD, EUR, CNY, GBP |
/rate USD EUR |
Указанные валюты |
Сетевые утилиты (/ip, /dns)
| Команда | Действие |
|---|---|
/ip |
Внешний IP сервера (api.ipify.org) |
/ip 8.8.8.8 |
Страна, город, org (ipinfo.io, 50k/мес free) |
/dns budaev.org |
DNS A/AAAA (socket.getaddrinfo) |
Мониторинг сайтов (/monitor)
Файл: ~/.config/deltabot/monitors.json
Воркер: monitor_worker() — проверяет каждые 5 минут, шлёт алерт всем подписчикам при смене статуса.
| Команда | Действие |
|---|---|
/monitor add <url> |
Добавить (мгновенная проверка при добавлении) |
/monitor list |
Список с последним статусом |
/monitor remove <N|url> |
Удалить по номеру или URL |
/monitor check |
Немедленная проверка всех |
Алерт при падении отправляется всем подписчикам (subscribers.json).
CalDAV напоминания
Воркер: cal_reminder_worker() — каждые 5 минут проверяет события на ближайшие 15 минут.
Файл отправленных: ~/.config/deltabot/cal_reminders_sent.json — {uid: timestamp}, очищается автоматически (старше 25ч).
Уведомления получают все подписчики (как утренний отчёт).
CalDAV — события дня в утреннем отчёте
Функция send_daily_status_worker() вызывает cal_list_events(days_ahead=1) и добавляет список событий к отчёту. При ошибке CalDAV — отчёт уходит без раздела событий (warning в лог).
Whisper — транскрипция голосовых сообщений
API: Cloudflare Workers AI @cf/openai/whisper
Триггер: любое входящее сообщение с file_mime_type начинающимся на audio/ и без текста.
Функция: transcribe_audio(file_path) → POST бинарного аудио на Cloudflare.
Использует уже имеющиеся CLOUDFLARE_ACCOUNT_ID и CLOUDFLARE_API_TOKEN из .env.
/ai summary
Субкоманда /ai summary — разовый вызов AI без изменения истории. Реализована в ai_agent.summarize_session(chat_id). Берёт последние 20 сообщений, просит модель кратко резюмировать диалог.
AI Chat (Cloudflare Workers AI + OpenRouter fallback)
Статус: работает
Файлы: ai_agent.py, ~/.config/deltabot/ai_sessions.json
Провайдеры:
- Cloudflare Workers AI (основной) — 10,000 нейронов/день, регистрация без карты, работает из России
- OpenRouter (fallback) — 50 RPD, при ошибке Cloudflare
Один запрос = один ответ.
Переменные окружения (.env)
CLOUDFLARE_ACCOUNT_ID=<account_id>
CLOUDFLARE_API_TOKEN=<api_token>
OPENROUTER_API_KEY=sk-or-... # только для fallback
Команды
| Команда | Действие |
|---|---|
/ai on |
Включить AI (любое сообщение → ответ модели) |
/ai off |
Выключить AI |
/ai status |
Модель, сообщений, ключ |
/model |
Список моделей / выбор модели |
/apikey [ключ] |
Установить ключ OpenRouter (для fallback) |
Модели
Cloudflare Workers AI
| ID | Нейронов/ответ | Ответов/день |
|---|---|---|
@cf/qwen/qwen3-30b-a3b-fp8 (по умолчанию) |
~20 | ~500 |
@cf/meta/llama-3.3-70b-instruct-fp8-fast |
~116 | ~86 |
@cf/meta/llama-3.1-8b-instruct-fast |
~20 | ~500 |
@cf/meta/llama-4-scout-17b-16e-instruct |
~40 | ~250 |
@cf/deepseek-ai/deepseek-r1-distill-qwen-32b |
~50 | ~200 |
@cf/moonshotai/kimi-k2-instruct |
~40 | ~250 |
@cf/aisingapore/gemma-sea-lion-v4-27b-it |
~40 | ~250 |
OpenRouter (fallback)
| ID | Контекст |
|---|---|
deepseek/deepseek-v4-flash:free |
1M |
moonshotai/kimi-k2.6:free |
262K |
minimax/minimax-m2.5:free |
262K |
openrouter/free |
auto-route |
Лимиты
- Cloudflare: 10,000 нейронов/день (см. таблицу выше)
- OpenRouter: 50 запросов/день, 20 RPM (только при падении Cloudflare)
Архитектура
process_message()
├── Cloudflare Workers AI (основной)
│ └── при ошибке → OpenRouter (fallback)
└── OpenRouter (если модель начинается не с @cf/)
Хранение
ai_sessions.json: {chat_id: {model, enabled, messages[], api_key}}
Контекст — последние 20 сообщений. API ключ OpenRouter хранится per-session или в OPENROUTER_API_KEY env.