delta-chat-bot/AGENTS.MD
2026-06-13 15:53:28 +08:00

33 KiB
Raw Blame History

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 для постов с медиа. Бот:

  1. Читает первый <enclosure> из RSS
  2. Если type начинается с image/ — скачивает эту картинку
  3. Прикрепляет к тексту поста (поле file в send_msg)
  4. Удаляет временный файл после отправки
  5. Если скачать не удалось — пост уходит без картинки

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:ПАРОЛЬ (username bot, не 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

Порядок настройки для нового виртуального ящика:

  1. Установить dovecot-lmtpd (отдельный пакет на Ubuntu)
  2. Создать конфиги LMTP и auth-passwdfile
  3. Раскомментировать !include auth-passwdfile.conf.ext в /etc/dovecot/conf.d/10-auth.conf
  4. Создать запись в /etc/dovecot/users (хэш через doveadm pw -s SHA512-CRYPT)
  5. Создать Maildir с uid/gid из userdb: mkdir -p /var/mail/vhosts/dc.budaev.org/macky/Maildir/{new,cur,tmp}; chown -R 5000:5000
  6. Добавить transport в Postfix: echo 'macky@dc.budaev.org lmtp:unix:private/dovecot-lmtp' >> /etc/postfix/transport && postmap /etc/postfix/transport
  7. Перезапустить dovecot + postfix

Известные грабли mail

  1. relayhostНЕ СТАВИТЬ. Бот аутентифицирован на submission:587, Postfix сам доставляет на внешние MX.
  2. smtp_tls_wrappermode / smtp_use_tls — ломают TLS. Не использовать.
  3. bot@dc.budaev.org vs bot — в sasl_passwd username должен быть bot, не bot@dc.budaev.org.
  4. key_id в БД — после DELETE keypairs.id autoincrement не переиспользуется. Убедись что key_id в config совпадает с реальным id в keypairs.
  5. Spamhaus PBL — IP 90.188.48.201 в списке, iCloud/Gmail могут отклонять почту пока PTR не заработает.
  6. dovecot-lmtpd — обязательный пакет на Ubuntu, без него LMTP сокет не создаётся.
  7. Group postfix vs _postfix — на Ubuntu пользователь postfix, на macOS _postfix.
  8. Permission denied autocreate Maildir — виртуальный ящик не может создать Maildir сам, нужно создать вручную с uid/gid, совпадающим с default_fields в userdb.
  9. 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 в файл, проверка в цикле.

Известные проблемы

  1. "key is missing" при отправке в новый чат — secure join handshake не завершён.
  2. Описание каналаset_chat_description требует core v2.50.0 (не released).
  3. Аватарка каналаset_image() может не работать на core v2.49.0.
  4. Greylisting на mail.budaev.org451-Greylisted, потом 421 Too many connections.
  5. Spamhaus PBL — PTR для 90.188.48.201 ещё не разрезолвился.
  6. tg.i-c-a.su ненадёжен — частые таймауты (17s+), per-channel баны, 15 RPM. PHP-прокси на budaev.org смягчает (кэш + fallback).
  7. Unbound на сервере — медленный резолвинг (122ms), рекомендуется форвардинг на 1.1.1.1.

Планы на оптимизацию

  1. Поддержка аватарок каналов (chat.set_image()).
  2. Очистка старых аккаунтов (~/.local/share/deltachat/accounts/).
  3. Автообновление LE сертификата (certbot renew + хуки).
  4. Уменьшить 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_INTERVAL 90→180с.
  • channels.json: синхронизирован с реальным списком (6 каналов: markettwits, raiznews, droidergram, gremtelegram, postnauka, kartiny2).

13.06.2026 — Security fixes

  • deltabot.py smtp_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_INTERVAL 180→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.