python3 -V), утилита curl, аккаунт Telegram и бот от @BotFather, доступ к crontab (или GitHub Actions), ~50 МБ свободного места под лог и файл состояния
sqlite3/xml.etree из стандартной библиотеки — внешние пакеты не нужны, поэтому версии зависимостей подбирать не придётся.curl для ручной проверки лент: curl --version./newbot, получите токен вида 123456789:AA... (значение ваше, не публикуйте)./opt/ai-watch (или любой свой) и редактировать свой crontab.Что отслеживать — определитесь до настройки. Ориентир по самим моделям: Лучшие нейросети и ИИ-модели 2026.
Чек-лист готовности:
python3 -V печатает версию без ошибокfeeds.jsonchat_idПример: проверка окружения одной строкой.
python3 -V && curl --version | head -n 1
Ожидаемый вывод — две строки, например Python 3.12.x и curl 8.x.x. Если вторая строка отсутствует, установите curl пакетным менеджером.
Что сделать: составьте файл feeds.json, где ключ — человекочитаемое имя источника, значение — URL ленты. Для GitHub не нужен API-ключ: у каждого репозитория есть Atom-лента релизов вида https://github.com/<owner>/<repo>/releases.atom.
Зачем: один скрипт читает разнородные источники одинаково — RSS 2.0 и Atom парсятся одним кодом, а имена ключей потом попадают в заголовок уведомления.
Как проверить: каждая лента должна отдавать HTTP 200 и XML, а не HTML-страницу.
Пример: feeds.json с реальным репозиторием библиотеки Transformers.
{
"transformers-releases": "https://github.com/huggingface/transformers/releases.atom",
"arxiv-cs-cl": "https://arxiv.org/rss/cs.CL"
}
Второй источник — пример научной ленты; доступность конкретной ленты проверьте в браузере, адреса и форматы могут меняться, сверьтесь с документацией источника.
curl -sI https://github.com/huggingface/transformers/releases.atom | head -n 3
curl -s https://arxiv.org/rss/cs.CL | head -c 120
Ожидаемо: первая команда печатает HTTP/2 200 (или HTTP/1.1 200), вторая — начало XML с тегом <rss. Если вместо XML пришёл <html>, лента недоступна или требует другого URL.
Почему это работает: GitHub отдаёт релизы как обычный Atom-фид, поэтому отдельный парсер GitHub не нужен — отличие только в тегах entry вместо item.
Если вы следите за открытыми весами моделей, список репозиториев удобно взять отсюда: Какие ИИ-модели имеют открытые веса и как их запустить бесплатно. Добавляйте в feeds.json только те репозитории, где релизы действительно публикуются.
| Источник | Что даёт | Авторизация | Частота опроса |
|---|---|---|---|
| GitHub `releases.atom` | номера версий, теги, changelog | не нужна | 1–2 раза в час |
| RSS 2.0 блога/хаба | анонсы моделей и статей | не нужна | раз в час |
| GitHub REST `/releases` | JSON, фильтры по `tag_name` | токен, лимиты — сверьтесь с документацией GitHub | по расписанию |

Что сделать: положите в каталог проекта скрипт watch.py. Он читает feeds.json, скачивает ленты, сравнивает идентификаторы записей с state.json и отправляет только новые записи в Telegram через Bot API.
Зачем: дедупликация по guid/id защищает от повторных уведомлений при каждом запуске cron; файл состояния переживает перезапуски.
Как проверить: запустите скрипт вручную и посмотрите на консольный вывод и сообщение в чате.
Пример: сохраните как watch.py.
#!/usr/bin/env python3
import json, os, urllib.request, urllib.parse
import xml.etree.ElementTree as ET
from pathlib import Path
STATE, FEEDS = Path("state.json"), Path("feeds.json")
TG_TOKEN, TG_CHAT = os.environ["TG_TOKEN"], os.environ["TG_CHAT_ID"]
def fetch(url):
req = urllib.request.Request(url, headers={"User-Agent": "ai-watch/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
return r.read()
def parse(xml_bytes):
root, items = ET.fromstring(xml_bytes), []
for it in root.iter("item"): # RSS 2.0
items.append({"id": (it.findtext("guid") or it.findtext("link") or "").strip(),
"title": (it.findtext("title") or "").strip(),
"link": (it.findtext("link") or "").strip()})
if not items: # Atom
ns = {"a": "http://www.w3.org/2005/Atom"}
for e in root.findall("a:entry", ns):
link = e.find("a:link", ns)
items.append({"id": (e.findtext("a:id", namespaces=ns) or "").strip(),
"title": (e.findtext("a:title", namespaces=ns) or "").strip(),
"link": link.get("href") if link is not None else ""})
return [i for i in items if i["id"]]
def tg_send(text):
data = urllib.parse.urlencode({"chat_id": TG_CHAT, "text": text,
"parse_mode": "HTML",
"disable_web_page_preview": "true"}).encode()
req = urllib.request.Request(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage", data=data)
with urllib.request.urlopen(req, timeout=30) as r:
return r.status
def main():
feeds = json.loads(FEEDS.read_text(encoding="utf-8"))
seen = json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {}
for name, url in feeds.items():
try:
items = parse(fetch(url))
except Exception as e:
print(f"[ERR] {name}: {e}")
continue
known = set(seen.get(name, []))
new = [i for i in items if i["id"] not in known]
for i in reversed(new):
tg_send(f"<b>{name}</b>\n{i['title']}\n{i['link']}")
seen[name] = list(known | {i["id"] for i in new})[-200:]
print(f"[OK] {name}: new={len(new)}")
STATE.write_text(json.dumps(seen, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
main()
Узнайте chat_id: отправьте боту сообщение и выполните запрос getUpdates.
curl -s "https://api.telegram.org/bot$TG_TOKEN/getUpdates"
В ответе найдите "chat":{"id":123456789,...} — это и есть TG_CHAT_ID. Затем запуск:
export TG_TOKEN="<токен-из-BotFather>"
export TG_CHAT_ID="<ваш-chat_id>"
python3 watch.py
Ожидаемый вывод при первом запуске: строки [OK] transformers-releases: new=<N> и [OK] arxiv-cs-cl: new=<N>, а в чате — пачка сообщений. Повторный запуск должен дать new=0.
Поставьте опрос на расписание — например, каждые 30 минут:
crontab -e
*/30 * * * * cd /opt/ai-watch && TG_TOKEN=<токен> TG_CHAT_ID=<chat_id> /usr/bin/python3 watch.py >> watch.log 2>&1
Альтернатива без своего сервера — GitHub Actions; секреты задаются в Settings → Secrets and variables → Actions, расписание в cron-синтаксисе:
name: watch
on:
schedule: [{cron: "0 */6 * * *"}]
workflow_dispatch:
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 watch.py
env:
TG_TOKEN: ${{ secrets.TG_TOKEN }}
TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}
Почему это работает: cron не наследует переменные из интерактивной оболочки, поэтому TG_TOKEN и TG_CHAT_ID указаны прямо в строке задания.

Что сделать: проведите три проверки — свежесть лога, отсутствие дублей и доставку тестового уведомления.
Зачем: молчащий бот неотличим от сломанного скрипта, поэтому нужен явный признак успеха на каждой итерации.
Как проверить:
tail -n 20 /opt/ai-watch/watch.log
docker --version 2>/dev/null | head -n 1
python3 - <<'PY'
import json, pathlib
s = json.loads(pathlib.Path("state.json").read_text(encoding="utf-8"))
print({k: len(v) for k, v in s.items()})
PY
Ожидаемый результат:
1. В watch.log появляются новые строки [OK] ... примерно с той же периодичностью, что задана в cron.
2. Словарь на экране содержит по ключу на каждый источник из feeds.json, счётчики не равны нулю.
3. Строк [ERR] в логе нет.
Тестовая доставка в обход расписания:
curl -s -X POST "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
-d chat_id="$TG_CHAT_ID" -d text="ai-watch: проверка канала доставки"
Ожидаемо: JSON {"ok":true,"result":{...}} и сообщение в чате. Если пришло "ok":false, смотрите раздел проблем.
Настройте ротацию, иначе лог вырастет незаметно:
sudo tee /etc/logrotate.d/ai-watch >/dev/null <<'EOF'
/opt/ai-watch/watch.log {
weekly
rotate 4
compress
missingok
notifempty
}
EOF
Почему это работает: logrotate переименовывает файл и создаёт новый, а cron продолжает писать в тот же путь.
Итог шага: у вас есть расписание, лог с меткой [OK] на каждый источник и подтверждённый канал доставки. Смежные сценарии автоматизации описаны в гайде по ИИ-помощнику для интернет-магазина на локальной модели — там тот же принцип «скрипт + расписание + внешний сервис».
Проблема. Лента не парсится, в логе ошибка синтаксиса XML
[ERR] arxiv-cs-cl: syntax error: line 1, column 0feeds.json, если лента недоступна.curl -sI "<URL-ленты>" | head -n 3
curl -s "<URL-ленты>" | head -c 200
Если видите HTTP/2 404 или <html> — URL устарел, сверьтесь с документацией источника.
Проблема. Бот не доставляет сообщения
{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}chat_id, либо вы ни разу не написали боту — он не может инициировать диалог первым.getUpdates, скопируйте message.chat.id целиком (он может быть отрицательным для групп) и обновите TG_CHAT_ID.curl -s "https://api.telegram.org/bot$TG_TOKEN/getUpdates" | grep -o '"chat":{"id":-\?[0-9]*'
Проблема. Cron падает сразу при старте
KeyError: 'TG_TOKEN'~/.bashrc, который cron не читает.TG_TOKEN и TG_CHAT_ID в строку задания cron, как в примере Шага 2, либо используйте EnvironmentFile в systemd-таймере.crontab -l | grep watch.py
Команда должна показать строку с обеими переменными перед /usr/bin/python3.
Вы получили автономный конвейер: feeds.json с источниками, watch.py с дедупликацией через state.json и cron, который раз в 30 минут отправляет новые записи в Telegram. Дальше добавьте источники, которых не хватает, и настройте фильтр по ключевым словам в заголовке, чтобы снизить шум.
Чем GitHub Releases отличается от RSS-ленты?
Ничем по механике: releases.atom — это Atom-фид, тот же формат, что и RSS, просто с тегами entry вместо item, поэтому один парсер читает оба.
Обязательно ли держать свой сервер?
Нет: тот же скрипт запускается по расписанию в GitHub Actions, секреты TG_TOKEN и TG_CHAT_ID хранятся в настройках репозитория.
Почему приходит старая запись повторно?
Скорее всего удалён или пересоздан state.json — идентификаторы потеряны. Скрипт считает все записи новыми; верните файл из резервной копии или удалите лишние сообщения вручную.
Какой интервал опроса выбрать?
Начните с 30–60 минут: этого достаточно для релизов и анонсов, а лимиты API и rate limit остаются в норме. Точные ограничения конкретного источника сверьте с его документацией.