800 непрочитанных в TG: дайджест за тебя
Агент читает 80+ каналов и присылает структурированную сводку каждое утро в 07:00. Четыре блока: Top-3 + дедупликация + мнения + вкусненькое. Полный стек, конфиги и код — внутри.
Что внутри гайда
- Полный profile.yaml — 35+ строк рабочего примера: биография, интересы, бизнес-контекст, регион, юрисдикция, игнор-лист
- Полный channels.yaml — 50+ строк с весами, vip_authors, fallback-источниками, дедуп-группами, кросс-канальными правилами
- Telethon-каркас — рабочий Python: auth, iter_messages за ночь, persist в Postgres, обработка FloodWait
- Embedding-пайплайн — BGE-M3 [verified 2026], cosine threshold 0.85, HDBSCAN-кластеризация с обоснованием выбора
- Полный prompt для Claude Haiku 4.5 — шаблон на суммаризацию кластера с источниками и цитатами
- DB-схема PostgreSQL — таблицы posts, clusters, digests, subscriptions с индексами и pgvector
- 7 граблей с боевого использования — spam-фильтр, session-revoke, rate-limit, длина 4096, кодировки, каналы без истории
Top-3 за ночь — то, на что среагировать сегодня
В 07:00 первый блок дайджеста — три абзаца. Каждый: новость + источник + цитата + почему важно тебе. Не «топ постов по лайкам» — а то, что важно именно тебе. Агент знает твои интересы из profile.yaml.
Telethon-сессия накапливает за ночь 300–500 сообщений из всех подписок. Агент не смотрит на лайки. Он смотрит на два сигнала.
Первый — кросс-источниковое упоминание: если одно событие встретилось в 5+ независимых каналах, это не шум. Второй — совпадение с твоим профилем: в profile.yaml ты один раз описываешь, что тебе важно. Агент взвешивает каждый кластер новостей по этому описанию.
Финальный отбор: топ-3 кластера по суммарному весу → Claude Haiku 4.5 пишет один абзац на каждый в формате «что случилось / почему важно / источник / цитата». Не придумывает — конспектирует из реального текста канала.
Полный profile.yaml
Заполняется один раз. Хранится в Postgres (таблица subscriptions). Агент читает при каждом запуске cron 06:55.
# profile.yaml — полный рабочий пример
# Заполняется один раз. Хранится в Postgres (таблица subscriptions).
# Агент читает при каждом запуске cron 06:55.
version: "1.0"
# --- Кто ты ---
bio:
name: "Тимур"
role: "Основатель"
company: "DeFiCap"
region: "Russia"
timezone: "Europe/Moscow"
language: "ru" # язык дайджеста
jurisdiction: # влияет на фильтр регуляции
primary: "RU"
secondary: ["EU", "UAE"]
# --- Основные интересы (core) ---
# Агент усиливает сигнал при попадании ключевых слов в кластер
interests:
defi:
keywords: ["Aave", "Pendle", "Morpho", "Curve", "EigenLayer", "restaking",
"yield", "liquidity", "TVL", "liquidation", "protocol risk"]
weight: 1.0 # максимальный приоритет
crypto_markets:
keywords: ["ETH", "BTC", "SOL", "funding rate", "open interest",
"spot ETF", "whale", "exchange outflow"]
weight: 0.9
regulation:
keywords: ["ЦБ РФ", "SEC", "MAS", "MiCA", "FATF", "лицензия",
"запрет", "стейблкоин", "CBDC"]
weight: 0.95 # высокий — регуляция влияет на бизнес
saas_ai:
keywords: ["Claude", "OpenAI", "Anthropic", "revenue", "ARR",
"agent", "RAG", "embedding", "MCP", "конкурент"]
weight: 0.7
business:
keywords: ["конверсия", "CAC", "LTV", "retention", "MRR",
"юнит-экономика", "Series A", "acquisition"]
weight: 0.75
# --- Смежные темы (secondary) — попадают в блок 4 «Вкусненькое» ---
adjacent:
- macro_economy: ["ФРС", "ставка", "инфляция", "рецессия", "DXY"]
- geopolitics: ["санкции", "заморозка", "SWIFT", "платёжная система"]
- startup_ecosystem: ["YC", "a16z", "Sequoia", "раунд", "стартап"]
- tech_infrastructure: ["AWS", "Cloudflare", "outage", "zero-day", "CVE"]
# --- Стоп-список ---
# Посты с этими признаками удаляются ДО эмбеддингов (экономия токенов)
ignore:
content_types:
- "конкурс"
- "реклама"
- "розыгрыш"
- "партнёрский материал"
- "промокод"
- "скидка"
patterns:
- "прогноз без данных" # фраза «BTC достигнет» без источника
- "мем"
- "репост без комментария"
- "утреннее приветствие"
- "добрый вечер"
min_length_chars: 80 # посты короче 80 символов — в мусор
# --- Качество дайджеста ---
digest:
top3_min_cross_mentions: 3 # минимум каналов для попадания в Top-3
max_items_block4: 7 # «Вкусненькое» — не больше 7 пунктов
max_opinions: 5 # блок «Мнения» — не больше 5 авторов
schedule_cron: "55 3 * * *" # 06:55 МСК = 03:55 UTC
lookback_hours: 9 # окно: с 22:00 вчера до 07:00 сегодня
Selection logic — три прохода
- Предфильтрация по стоп-листу. Посты из
ignoreудаляются до эмбеддингов. Это экономит ~30% токенов. - Взвешивание кластеров. Каждый кластер получает
score = (cross_mention_count × channel_weight) × topic_relevance_score.topic_relevance_scoreвычисляется через cosine similarity вектора кластера с вектором конкатенации keywords из profile.yaml. - Финальный ранжинг. Кластеры сортируются по score DESC. Топ-3 идут в блок 1. Позиции 4–15 — в блок 2 (факты). Остальные — в блок 4.
Prompt-template для Claude Haiku 4.5
SYSTEM:
Ты — редактор дайджеста. Твоя задача: суммировать кластер новостей в один абзац
для занятого фаундера. Язык — русский. Стиль — деловой, без воды.
USER:
Ниже — {{cluster_size}} постов из {{channel_count}} каналов об одном событии.
Тема профиля читателя: {{top_interest_label}}.
ПОСТЫ (формат: [канал] текст):
{{posts_block}}
Напиши один абзац (3–5 предложений) в формате:
1. Что произошло (факт, цифра, дата).
2. Почему важно — именно для контекста "{{top_interest_label}}".
3. Источник: [@канал_с_наибольшим_весом](ссылка).
4. Цитата ≤ 2 предложений из первоисточника (дословно, в кавычках).
НЕ придумывай факты. Если цифры нет в постах — не пиши цифру.
НЕ начинай абзац со слова "Итак" или "Таким образом".
🔴 TOP-3 ЗА НОЧЬ
1/ Pendle TVL упал на 18% за 48 часов после обнаружения уязвимости в контракте
PT-токенов на Arbitrum. Команда приостановила новые депозиты. Текущий TVL —
$890M (с $1.08B). Источник: @defillama.
«Уязвимость затрагивает только SY-wrapper для Aave v3 markets. Withdraw
активен, новые позиции временно заблокированы.»
2/ SEC выдала Wells Notice трём DEX-протоколам — впервые юрисдикция распространяется
на полностью децентрализованные AMM. Список протоколов и сроки ответа SEC указаны
в первоисточнике. Источник: @blockworks_research.
«This is the first time the SEC has formally targeted protocols with no central
operating company. Response window: 30 days.»
3/ Anthropic закрыла раунд $3.5B при оценке $61B (от $40B в марте). Лид-инвестор —
Lightspeed. Среди соинвесторов — General Catalyst, Bessemer, MGX. Источник: @stratechery.
«Anthropic is now the second most valuable private AI company, behind only OpenAI.
The round signals strong enterprise traction for Claude.»
40 фактов вместо 800 постов
800 непрочитанных — это не 800 разных новостей. Это одна новость, пересказанная в 15 каналах, плюс 200 постов воды, рекламы и «коллеги, ловите важное». Блок 2 схлопывает всё это в 40 фактов.
Дедупликация — это не просто «удали дубли». Это сжатие информационного пространства с сохранением лучшего представителя каждой темы. Три этапа: эмбеддинги → кластеризация → выбор представителя.
Выбор модели — BGE-M3 [verified 2026]
Рекомендуемая модель: BAAI/bge-m3 (sentence-transformers). Почему именно она, а не paraphrase-multilingual-MiniLM-L12-v2 (упомянутая в v1 brief):
- MiniLM-L12-v2 обучена на 2021–2022 данных, не покрывает DeFi-терминологию и современные тикеры
- BGE-M3 поддерживает 100+ языков, включая русский и английский одновременно (твои каналы — микс)
- MTEB Multilingual 2026: BGE-M3 достигает 63.0 average score vs ~57.x у MiniLM-L12-v2
- Размер: 570MB — умещается в RAM бюджетного VPS (1–2GB)
- Поддерживает входы до 8192 токенов (длинные эссе не обрезаются)
- Альтернатива для совсем бюджетного VPS (512MB RAM):
intfloat/multilingual-e5-small(117MB, качество ниже ~8%)
Пайплайн дедупликации — 4 шага
- Embed. Каждый пост прогоняется через
bge-m3. Батчи по 32 поста. Результат — вектор float32[1024]. - Cluster. HDBSCAN с
min_cluster_size=2,min_samples=1,metric='cosine'. Порог эффективно задаётся черезcluster_selection_epsilon=0.15(= 1 − cosine_threshold 0.85). - Select representative. Из каждого кластера выбирается пост от канала с наибольшим
weightв channels.yaml. При равных весах — более длинный пост. - Filter noise. Посты, помеченные HDBSCAN как шум (
label=-1), прогоняются через Haiku 4.5: «это факт/аргумент с данными или вода?». Оставляемis_signal=true.
Почему HDBSCAN, а не агломеративная кластеризация
- Агломеративная требует заранее указать число кластеров K — ты не знаешь, сколько тем будет за ночь
- HDBSCAN автоматически определяет число кластеров и помечает выбросы (шум) — то, что нужно для фильтрации воды
- На 500–1000 постов HDBSCAN работает за 0.3–1.5 секунды
- Единственный минус: нестабильность при очень малых данных (<50 постов) — добавляй fallback на cosine pairwise если кластеров < 3
Порог cosine similarity — почему именно 0.85
- 0.90 → режет слишком много уникального, теряем разные углы одной темы
- 0.85 → хорошо объединяет «один и тот же факт из разных каналов», но сохраняет «разные реакции на один факт»
- 0.80 → сливает разные темы из одного смыслового домена (напр., «ETH упал» + «BTC упал» = один кластер)
В HDBSCAN: cluster_selection_epsilon = 1 - 0.85 = 0.15.
Python: embedding + HDBSCAN snippet
from sentence_transformers import SentenceTransformer
import hdbscan
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# Загружаем модель один раз при старте (кешируется в ~/.cache/torch/sentence_transformers)
model = SentenceTransformer("BAAI/bge-m3")
def deduplicate_posts(posts: list[dict]) -> list[dict]:
"""
posts: список словарей с ключами 'id', 'text', 'channel', 'channel_weight'
Возвращает: список представителей кластеров + одиночные посты
"""
if len(posts) < 2:
return posts
texts = [p["text"][:512] for p in posts] # bge-m3 отлично справляется с 512 символами
# Шаг 1: эмбеддинги батчами
embeddings = model.encode(
texts,
batch_size=32,
show_progress_bar=False,
normalize_embeddings=True # L2-нормализация → dot product = cosine similarity
)
# Шаг 2: HDBSCAN кластеризация
clusterer = hdbscan.HDBSCAN(
min_cluster_size=2,
min_samples=1,
metric="euclidean", # после L2-нормализации euclidean ≈ cosine distance
cluster_selection_epsilon=0.15, # = 1 - cosine_threshold(0.85)
cluster_selection_method="eom"
)
labels = clusterer.fit_predict(embeddings)
# Шаг 3: выбор представителя из каждого кластера
unique_labels = set(labels) - {-1} # -1 = шум (одиночные посты)
representatives = []
for label in unique_labels:
cluster_indices = np.where(labels == label)[0]
cluster_posts = [posts[i] for i in cluster_indices]
# Берём пост от канала с наибольшим весом
best = max(cluster_posts, key=lambda p: (p["channel_weight"], len(p["text"])))
best["cluster_size"] = len(cluster_posts)
best["cluster_channels"] = list({p["channel"] for p in cluster_posts})
representatives.append(best)
# Шаг 4: одиночные посты (шум) — передаём на Haiku-фильтр отдельно
noise_posts = [posts[i] for i in range(len(posts)) if labels[i] == -1]
return representatives, noise_posts
- Каскадные ссылки на один источник. Десять каналов пересылают один пост из @криптопанорамы с пометкой «важно» — тексты почти идентичны. BGE-M3 объединяет их в один кластер. Представитель — оригинал (наибольший вес). Проблема: если @криптопанорама нет в твоём channels.yaml — её вес = 0, представителем станет пересыльщик. Решение: добавить базовые новостные каналы в channels.yaml даже с низким весом.
- Один автор — несколько постов на одну тему. Если VIP-автор написал тред из 5 постов — они попадут в один кластер. Только первый станет представителем. Добавь пост-агрегацию для vip_authors: конкатенация всех постов одного автора за ночь перед эмбеддингом.
- Короткие посты (<80 символов). Сообщения вида «ETH $3200» дают нестабильные эмбеддинги. Стоп-список
min_length_chars: 80в profile.yaml решает это до кластеризации.
3–5 голосов, к которым прислушиваешься
Не все каналы равны. Есть авторы, у которых ты читаешь каждый пост вручную — если бы успевал. Агент читает их за тебя и выносит отдельным блоком, минуя дедупликатор.
В конфиге каналов — отдельный список vip_authors. Это не «важные каналы» — это люди, чьи суждения ты хочешь видеть без фильтрации кластеризатором. Их посты идут в блок 3 напрямую. Если автор ничего не написал за ночь — его строки в дайджесте нет.
Зачем отдельно, а не в Top-3? Потому что мнение VIP-автора может не быть «новостью» в классическом смысле — но ты хочешь его видеть. Разделение сохраняет баланс: блок 1 = факты, блок 3 = позиции.
Полный channels.yaml
Хранится рядом с profile.yaml или в Postgres (таблица subscriptions, поле config_json).
# channels.yaml — полный рабочий пример
# Хранится рядом с profile.yaml или в Postgres (таблица subscriptions, поле config_json).
version: "1.0"
# --- VIP-авторы ---
# Не участвуют в дедупликации. Их посты идут напрямую в блок 3 «Мнения».
# Если за ночь не писали — строки нет.
vip_authors:
- id: "@arthur_hayes"
name: "Arthur Hayes"
weight: 1.0
topics: ["macro", "crypto markets", "DeFi"]
note: "Основатель BitMEX, пишет лонгриды — агрегировать всю ночь в один блок"
- id: "@delphi_digital"
name: "Delphi Digital"
weight: 0.9
topics: ["DeFi research", "on-chain analytics"]
note: "Исследования, берём первый пост ночи если несколько"
- id: "@punk6529"
name: "punk6529"
weight: 0.85
topics: ["NFT", "decentralization", "regulation"]
note: "Иногда пишет треды — конкатенировать"
- id: "@defidegen_ru"
name: "DeFi Деген RU"
weight: 0.8
topics: ["DeFi RU", "yield", "стратегии"]
note: "Лучший русскоязычный DeFi-канал"
- id: "@your_mentor_handle"
name: "Имя ментора"
weight: 1.0
topics: ["business", "strategy"]
note: "Личный — всегда в блок 3"
# --- Основные каналы (участвуют в кластеризации) ---
# weight: 1.0 = наивысший приоритет представителя кластера
# weight: 0.5 = участвует, но не становится представителем при наличии сильных
channels:
# DeFi / крипто — первичные
- id: "@defillama"
weight: 1.0
lang: "en"
topics: ["TVL", "DeFi stats", "protocol news"]
dedup_group: "defi_news" # кросс-канальная группа для дедупликации
- id: "@thedefiant"
weight: 0.9
lang: "en"
topics: ["DeFi news", "regulation"]
dedup_group: "defi_news"
- id: "@coindesk"
weight: 0.85
lang: "en"
topics: ["crypto markets", "regulation", "macro"]
dedup_group: "crypto_news"
- id: "@cointelegraph"
weight: 0.8
lang: "en"
topics: ["crypto news"]
dedup_group: "crypto_news"
note: "Высокий объём — часто публикуют одно и то же дважды"
- id: "@blockworks_research"
weight: 0.9
lang: "en"
topics: ["DeFi research", "protocol analysis"]
dedup_group: "defi_news"
# Русскоязычные
- id: "@cryptodefix"
weight: 0.75
lang: "ru"
topics: ["крипто новости RU", "DeFi"]
dedup_group: "crypto_ru"
- id: "@ru_defi"
weight: 0.7
lang: "ru"
topics: ["DeFi RU"]
dedup_group: "crypto_ru"
# Макро / рынки
- id: "@unusual_whales"
weight: 0.85
lang: "en"
topics: ["options flow", "macro", "регуляция США"]
dedup_group: "macro"
- id: "@zerohedge"
weight: 0.6
lang: "en"
topics: ["macro", "финансы"]
dedup_group: "macro"
note: "Сенсационный тон — снижен вес"
# Регуляция
- id: "@coinbase_inst"
weight: 0.85
lang: "en"
topics: ["регуляция", "institutional"]
dedup_group: "regulation"
# --- Fallback-источники ---
# Если основной канал недоступен (приватный, забанен, rate-limit) — пробуем fallback
fallback:
defi_news:
- "@messari_crypto"
- "@rekt_news"
crypto_news:
- "@bitcoinmagazine"
crypto_ru:
- "@bits_ru"
# --- Кросс-канальная дедупликация ---
# Каналы одной dedup_group конкурируют за статус представителя кластера.
# Победитель: наибольший weight. Остальные — в поле cluster_channels для справки.
dedup_rules:
same_group_threshold: 0.82 # чуть мягче общего 0.85 — каналы одной ниши часто перефразируют
cross_group_threshold: 0.85 # стандартный порог между группами
vip_bypass: true # vip_authors всегда минуют дедуп независимо от threshold
Aggregation logic — как агент собирает блок «Мнения»
- Читаем все посты
vip_authorsза lookback-окно (9 часов) - Конкатенируем треды одного автора в один блок текста
- Передаём в Haiku 4.5 на суммаризацию (один запрос на автора)
- Если автор не писал — строки в дайджесте нет (не «не написал», а вообще отсутствует)
- Если мнения авторов конфликтуют по одной теме — Haiku 4.5 получает специальный prompt с инструкцией показать оба угла
- Лимит на блок 3:
max_opinions: 5авторов (задаётся в profile.yaml)
Prompt-template для секции «Мнения»
SYSTEM:
Ты — редактор раздела «Мнения» в дайджесте. Задача: выжать суть из постов
авторитетного автора за ночь. Язык — русский. Максимальная длина на автора: 3 предложения.
USER:
Автор: {{author_name}} ({{author_handle}})
Число постов за ночь: {{post_count}}
ПОСТЫ (хронологически):
{{posts_block}}
Напиши в формате:
**{{author_name}}** — [одно предложение: о чём написал за ночь].
Цитата: «{{дословная цитата ≤ 30 слов из наиболее важного поста}}»
→ [ссылка на оригинал]
Если постов не было — верни пустую строку (не пиши "не написал").
Если посты на разные темы — выбери наиболее резонансную для DeFi/крипто/бизнеса.
- Ты читаешь вручную, когда замечаешь новый пост
- Автор даёт оценку/позицию, а не только пересказывает
- Пишет реже чем 5 раз в день (иначе засорит блок 3)
- Опыт в твоей нише > 3 лет
- Начни с 3 авторов. Максимум — 5. Больше = блок 3 теряет ценность.
Не критично, но интересно
Не всё попадает в Top-3. Есть длинные эссе, нестандартные ниши, идеи из смежных отраслей — то, что не горит, но мозг скажет спасибо. Блок 4 — это «полка для чтения в самолёте».
В блок 4 идут посты, которые прошли фильтр воды (реальный факт/аргумент), но набрали недостаточно кросс-упоминаний для Top-3 и не являются VIP-мнением. Лимит: не больше 7 пунктов за раз.
Критерии — что попадает в блок 4
Haiku 4.5 помечает тип контента для блока 4:
long_read— текст > 800 слов или ссылка на статью/эссеadjacent— тема из секцииadjacentв profile.yaml (смежная, не ядро)curiosity— нестандартная ниша, источник за пределами основных каналовdata_point— интересная цифра или датасет, не ставший Top-3
Формат блока 4 в дайджесте: список с тегом типа, заголовком в 10 слов и ссылкой. Без суммаризации — только навигация. Читать или нет — твоё усмотрение.
Лимиты
- Максимум 7 пунктов (задаётся в profile.yaml:
max_items_block4) - Минимальный балл нишевости: если пост уже охвачен в блоке 2 — в блок 4 не попадает
- Тред из 5 постов считается как 1 пункт (конкатенация)
adjacent интересы.Telethon + Postgres + Claude Haiku 4.5 + cron
Агент работает 24/7 на VPS за $5–10. Стек выбирался под одно требование: максимум пользы при минимуме токенов. Вот почему именно эти инструменты — с реальным кодом, не псевдокодом.
Telethon, не бот. Telegram Bot API не даёт читать сообщения из чужих каналов — только принимать. Нужна именно user-сессия через Telethon: агент логинится как ты, слушает подписки и читает историю. Авторизация один раз, сессия хранится в файле .session, работает без перезапуска месяцами.
Postgres, не файлы. Все сообщения за сутки пишутся в таблицу posts, кластеры — в clusters, итоговые дайджесты — в digests. Это даёт историю: можно смотреть дайджест за любую дату, считать метрики качества.
Claude Haiku 4.5, не Sonnet. [verified 2026] Цены: $1.00 на входе / $5.00 на выходе за 1M токенов. Дайджест 40 фактов — это не архитектурная задача. Haiku 4.5 суммирует отлично. Стоимость одного утреннего запуска: ~$0.05–0.15. Sonnet здесь в 3–6 раз дороже без выигрыша в качестве.
Telethon-каркас: digest_collector.py
Telethon 1.43.x [verified 2026]. Репозиторий архивирован автором 21.02.2026, но 1.43.2 работает стабильно. Мониторь Pyrogram как альтернативу для v2.
#!/usr/bin/env python3
"""
digest_collector.py — Telethon-каркас для сбора сообщений за ночь.
Telethon 1.43.x [verified 2026]. Репозиторий архивирован автором 21.02.2026,
но 1.43.2 работает стабильно. Мониторь Pyrogram как альтернативу для v2.
"""
import asyncio
from datetime import datetime, timedelta, timezone
from telethon import TelegramClient
from telethon.errors import FloodWaitError, ChannelPrivateError
import psycopg2
import yaml
import os
API_ID = int(os.environ["TG_API_ID"])
API_HASH = os.environ["TG_API_HASH"]
DB_DSN = os.environ["DATABASE_URL"] # postgresql://user:pass@localhost/digest_db
SESSION = "digest_session" # файл digest_session.session рядом со скриптом
async def collect_channel(client, channel_id: str, since: datetime, channel_weight: float, conn):
"""Читаем сообщения канала за период через iter_messages."""
try:
async for message in client.iter_messages(
channel_id,
offset_date=datetime.now(timezone.utc), # от текущего момента назад
reverse=False,
limit=500 # hard cap на канал
):
if message.date < since:
break # iter_messages идёт от новых к старым — вышли за окно
if not message.text or len(message.text) < 80:
continue # стоп-лист по min_length
cur = conn.cursor()
cur.execute("""
INSERT INTO posts (channel_id, channel_weight, message_id, text, posted_at, collected_at)
VALUES (%s, %s, %s, %s, %s, NOW())
ON CONFLICT (channel_id, message_id) DO NOTHING
""", (channel_id, channel_weight, message.id, message.text, message.date))
conn.commit()
except ChannelPrivateError:
print(f"[WARN] {channel_id} приватный или недоступен — пропускаем")
except FloodWaitError as e:
print(f"[WARN] FloodWait {e.seconds}s на {channel_id} — ждём...")
await asyncio.sleep(e.seconds + 5)
async def main():
with open("channels.yaml") as f:
config = yaml.safe_load(f)
since = datetime.now(timezone.utc) - timedelta(hours=9) # lookback 9ч
conn = psycopg2.connect(DB_DSN)
async with TelegramClient(SESSION, API_ID, API_HASH) as client:
for ch in config["channels"]:
await collect_channel(client, ch["id"], since, ch["weight"], conn)
await asyncio.sleep(0.5) # пауза между каналами — избегаем rate-limit
# Отдельно: VIP-авторы
for vip in config["vip_authors"]:
await collect_channel(client, vip["id"], since, vip["weight"], conn)
await asyncio.sleep(0.5)
conn.close()
if __name__ == "__main__":
asyncio.run(main())
DB-схема PostgreSQL
-- digest_db: полная схема PostgreSQL
-- pgvector extension: CREATE EXTENSION IF NOT EXISTS vector;
-- Сырые сообщения из каналов
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
channel_id TEXT NOT NULL, -- '@defillama'
channel_weight FLOAT NOT NULL DEFAULT 0.5,
message_id BIGINT NOT NULL,
text TEXT NOT NULL,
posted_at TIMESTAMPTZ NOT NULL,
collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
embedding VECTOR(1024), -- pgvector, заполняется после collect
is_vip_author BOOLEAN DEFAULT FALSE,
is_noise BOOLEAN DEFAULT NULL, -- NULL=не проверено, TRUE=шум, FALSE=сигнал
UNIQUE(channel_id, message_id)
);
CREATE INDEX idx_posts_posted_at ON posts (posted_at DESC);
CREATE INDEX idx_posts_channel ON posts (channel_id);
CREATE INDEX idx_posts_embedding ON posts USING ivfflat (embedding vector_cosine_ops);
-- Кластеры (результат HDBSCAN)
CREATE TABLE clusters (
id BIGSERIAL PRIMARY KEY,
digest_date DATE NOT NULL,
cluster_label INT NOT NULL, -- -1 = noise
representative_post_id BIGINT REFERENCES posts(id),
post_ids BIGINT[] NOT NULL, -- все посты кластера
channel_ids TEXT[] NOT NULL, -- список каналов в кластере
cluster_size INT NOT NULL DEFAULT 1,
topic_score FLOAT, -- релевантность profile.yaml
block_assignment INT, -- 1=Top3, 2=facts, 4=tasty, 3=vip(не кластер)
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_clusters_date ON clusters (digest_date DESC);
-- Готовые дайджесты
CREATE TABLE digests (
id BIGSERIAL PRIMARY KEY,
digest_date DATE NOT NULL UNIQUE,
block1_text TEXT, -- Top-3 (уже отформатировано Haiku)
block2_text TEXT, -- 40 фактов
block3_text TEXT, -- Мнения VIP
block4_text TEXT, -- Вкусненькое
full_text TEXT, -- итоговое сообщение для TG
tg_message_id BIGINT, -- ID отправленного сообщения в TG
tg_sent_at TIMESTAMPTZ,
stats_json JSONB, -- {posts_total, clusters_count, ...}
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Подписки и конфиги пользователей (для мульти-юзер расширения)
CREATE TABLE subscriptions (
id BIGSERIAL PRIMARY KEY,
tg_user_id BIGINT NOT NULL UNIQUE,
tg_username TEXT,
profile_yaml TEXT NOT NULL, -- сериализованный profile.yaml
channels_yaml TEXT NOT NULL, -- сериализованный channels.yaml
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Cron setup
# Добавить в crontab: crontab -e
# Формат: минута час день месяц день_недели команда
# 06:55 МСК = 03:55 UTC
# Шаг 1 — сбор сообщений (запускается в 06:30 МСК = 03:30 UTC для запаса)
30 3 * * * cd /opt/digest && /opt/digest/venv/bin/python digest_collector.py >> /var/log/digest/collector.log 2>&1
# Шаг 2 — кластеризация + суммаризация + отправка (06:55 МСК = 03:55 UTC)
55 3 * * * cd /opt/digest && /opt/digest/venv/bin/python digest_aggregator.py >> /var/log/digest/aggregator.log 2>&1
# Проверка работоспособности — если лог не обновлялся > 2ч, шлём алерт
0 5 * * * /opt/digest/scripts/health_check.sh
Отправка дайджеста в TG
Для отправки нужен отдельный бот (не Telethon-сессия). Создай через @BotFather, получи TOKEN. Webhook не нужен — дайджест отправляется по расписанию.
# digest_sender.py — вызывается из aggregator.py
import requests, time, os
from datetime import datetime
BOT_TOKEN = os.environ["TG_BOT_TOKEN"]
CHAT_ID = os.environ["TG_OWNER_CHAT_ID"] # твой Telegram user_id
def send_digest(text: str):
"""Разбиваем на части по 4000 символов с запасом."""
parts = [text[i:i+4000] for i in range(0, len(text), 4000)]
for i, part in enumerate(parts):
prefix = f"📬 Дайджест {datetime.now().strftime('%d.%m')} [{i+1}/{len(parts)}]\n\n" if len(parts) > 1 else ""
response = requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={
"chat_id": CHAT_ID,
"text": prefix + part,
"parse_mode": "Markdown",
"disable_web_page_preview": True
}
)
if response.status_code == 429:
retry_after = response.json().get("parameters", {}).get("retry_after", 30)
time.sleep(retry_after + 2)
# повторная попытка
client.session.set_dc() на залогиненной Telethon-сессии. Это немедленно отзывает авторизацию: сессия становится невалидной, при попытке переподключения Telegram выкидывает FloodWaitError или AUTH_KEY_UNREGISTERED. Смена DC нужна только при первичной авторизации — и то, только если сам Telegram её запрашивает. Увидел вызов set_dc() в чужом коде — убери до деплоя. [memory-from-Timur] Это подтверждено на боевом проекте: revoke сессии после вызова set_dc() — реальный сценарий, не теоретический.7 граблей с боевого использования
Агент не сломается на первый день. Но через неделю ты встретишь их всех. Вот полный список граблей с симптомами и фиксами.
Ни один из этих сценариев не описан в официальной документации Telegram или Telethon. Всё ниже — боевой опыт.
- Spam-фильтр (нежелательная реклама в дайджесте).
Симптом: в блок 2 «фактов» попадают посты вида «Этот протокол даёт 40% годовых, регистрируйся по реферальной ссылке».
Причина: рекламные посты содержат реальные цифры — фильтр воды (Haiku) ошибается.
Фикс: добавить URL-паттерн в стоп-список (?ref=,utm_source,t.me/+invite). Отдельным шагом до эмбеддинга проверять наличие гиперссылок в тексте — если ссылка есть в >60% схожих постов от нового канала, снижать вес канала автоматически. - Session revoke (сессия Telethon умерла).
Симптом:AuthKeyUnregisteredErrorилиSessionPasswordNeededErrorв логах. Дайджест не приходит.
Причина: (а) ты залогинился в Telegram на новом устройстве и приложение сбросило старые сессии; (б) вызовset_dc()на активной сессии [КРИТИЧНО]; (в) аккаунт был временно ограничен Telegram.
Фикс: мониторинг черезhealth_check.sh. ПриAuthKeyError— отправить TG-уведомление тебе через резервный бот-токен. Реавторизация вручную занимает 2 минуты — главное вовремя узнать. - TG rate-limit на чтение (FloodWaitError).
Симптом: Telethon бросаетFloodWaitError: A wait of 300 seconds is required. Сбор сообщений прерывается.
Причина: слишком быстрые запросы к нескольким каналам подряд. [verified 2026] Telegram не публикует лимиты официально, но практика: не более 20–30 каналов в минуту приiter_messages.
Фикс:await asyncio.sleep(0.5)между каналами (уже в скрипте выше). ПриFloodWaitError—await asyncio.sleep(e.seconds + 5)и продолжать со следующего канала. - Длина сообщения 4096 символов.
Симптом: Bot API возвращает ошибку400 Bad Request: message is too long.
Причина: дайджест 40 фактов + Top-3 + мнения легко превышает 4096 символов.
Фикс: разбивка на части по 4000 символов (уже вsend_digest()выше). Дополнительно: отправляй каждый блок отдельным сообщением (Block 1, Block 2, Block 3, Block 4) — читать удобнее. - Мульти-аккаунт и параллельные сессии.
Симптом: если запустить два экземпляра коллектора одновременно —AuthKeyDuplicatedError.
Причина: Telethon не поддерживает параллельные подключения с одним SESSION-файлом.
Фикс: lockfile перед стартом коллектора:fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB). Если блокировка не получена — завершаем процесс. - Проблемы с кодировкой (encoding errors).
Симптом:UnicodeDecodeErrorпри записи в Postgres или при обработке текста.
Причина: TG-посты содержат emoji (4-байтовые символы Unicode), экзотические символы, нулевые байты.
Фикс: при вставке в Postgres:text.replace('\x00', ''). Убедись, что БД создана сENCODING='UTF8'иLC_COLLATE='en_US.UTF-8'. - Каналы без истории (restricted history).
Симптом:iter_messagesвозвращает 0 постов для некоторых каналов.
Причина: часть каналов включает ограничение «показывать историю только участникам последних X дней». Telethon-сессия (user-аккаунт) может не иметь доступа к старым постам.
Фикс: добавить флагaccessible: falseпри первой попыткеiter_messagesвернуть 0 — и использовать fallback-канал из channels.yaml. Либо подключить Telethon-сессию аккаунта, который давно подписан на эти каналы.
digests, шлём тебе TG-алерт через резервный бот (отдельный токен, отдельный аккаунт). Так ты узнаешь о поломке через 5 минут после того, как должен был получить дайджест.Как замерить, что дайджест работает
«Дайджест хороший» — это не метрика. Метрика — сколько важных событий ты пропустил и сколько времени потратил на дайджест вместо скроллинга.
Без измерений агент деградирует: промпты ломаются, каналы меняют тематику, пороги перестают работать. Эти 6 метрик позволяют заметить деградацию до того, как она стала проблемой.
6 метрик качества
- Compression ratio (сжатие). Формула:
unique_clusters / total_posts_collected. Норма: ≤ 0.06 (800 постов → ≤ 48 кластеров). Если ratio растёт → каналы стали разнообразнее (хорошо) или порог кластеризации слишком высокий (плохо). Если падает → много дублирующих каналов, стоит почистить channels.yaml. - Miss rate (пропуски важного). Раз в неделю открывай TG вручную на 10 минут. Считай, сколько важных постов не попало в дайджест. Норма: ≤ 2 пропуска в неделю из «критично важных». Если выше: снижай
top3_min_cross_mentionsс 3 до 2, или добавь канал-источник в channels.yaml. - False positive rate (вода в дайджесте). Отмечай посты в блоке 2 как «полезно» / «вода» в течение недели (3–5 оценок). Норма: ≤ 15% воды в блоке 2 (6–7 из 40 фактов = нормально). Если выше: ужесточи стоп-список.
- Time-to-read (время чтения дайджеста). Норма: 5–8 минут на полный дайджест. Если выше 12 минут → дайджест слишком длинный, снижай лимиты блоков. Технически: считай символы в
full_textиз таблицыdigests. ~200 символов = ~1 минута чтения. - Retention (возвращаемость). Сколько дней подряд ты открыл и прочитал дайджест. Норма: ≥ 5 из 7 дней на второй неделе после запуска. Если ниже: скорее всего Top-3 нерелевантны — пересмотри
interestsв profile.yaml. - Source diversity (разнообразие источников). SQL:
SELECT channel_id, COUNT(*) FROM clusters WHERE digest_date > NOW()-'7d'::interval GROUP BY channel_id ORDER BY 2 DESC. Норма: ни один канал не занимает > 20% кластеров. Если выше: этот канал доминирует → снизь его weight или добавь контрбалланс.
Бенчмарки — что считать «хорошим» дайджестом
- Compression ratio: плохо > 0.10 · норма 0.04–0.06 · отлично < 0.04
- Miss rate (важные): плохо > 3/нед · норма 1–2/нед · отлично 0/нед
- False positive (вода): плохо > 25% · норма 10–15% · отлично < 10%
- Time-to-read: плохо > 15 мин · норма 5–8 мин · отлично 3–5 мин
- Retention: плохо < 50% · норма 70–80% · отлично > 85%
От нуля до первого дайджеста — 7 шагов
Агент разворачивается за вечер (3–4 часа). Вот последовательность, которая не ломается.
- Шаг 1 — Получить Telegram API credentials (10 мин).
Зайди на my.telegram.org → API development tools. Создай приложение, получиapi_idиapi_hash. Запиши в.env:TG_API_ID=...,TG_API_HASH=.... Создай бота через @BotFather, запишиTG_BOT_TOKEN=.... Узнай свойTG_OWNER_CHAT_IDчерез @userinfobot. - Шаг 2 — Развернуть Postgres на VPS (20 мин).
apt install postgresql python3-pip python3-venv.createdb digest_db && psql digest_db < schema.sql(схема из section 5). Установить расширение pgvector если будешь хранить эмбеддинги в БД:CREATE EXTENSION vector;. Зависимости:pip install telethon sentence-transformers hdbscan psycopg2-binary anthropic pyyaml. - Шаг 3 — Заполнить profile.yaml и channels.yaml (30 мин).
Скопируй примеры из section 1 и section 3 этого гайда. Начни с 10–15 каналов (можно расширить позже). Укажи 2–3 VIP-авторов (не больше на старте). Заполниinterestsчестно — не «всё интересно», а что реально влияет на решения. - Шаг 4 — Авторизовать Telethon-сессию (5 мин).
Запустиpython digest_collector.pyвручную первый раз. Telethon запросит номер телефона и код из SMS/приложения. После успешной авторизации появитсяdigest_session.session— это твой токен, храни как секрет. НИКОГДА не перемещай этот файл на другой сервер без реавторизации. - Шаг 5 — Тестовый запуск коллектора (15 мин).
Запустиdigest_collector.pyвручную, проверь в Postgres:SELECT COUNT(*) FROM posts;. Должно появиться 50–500 постов в зависимости от активности каналов. Если 0: проверь, что аккаунт подписан на указанные каналы вручную в Telegram. - Шаг 6 — Запустить агрегатор и проверить дайджест (30 мин).
Запустиdigest_aggregator.pyвручную (кластеризация + Claude Haiku 4.5). Проверь вывод: три блока текста, ссылки рабочие, нет hallucinated фактов. Настройanthropicклиент:pip install anthropic,ANTHROPIC_API_KEY=.... Если качество плохое — пересмотриinterestsв profile.yaml (самая частая причина). - Шаг 7 — Настроить cron и мониторинг (15 мин).
Добавь cron-строки из section 5. Создай/var/log/digest/с правами записи. Добавь health_check: еслиdigestsне обновился сегодня к 07:05 — отправь TG-сообщение через запасной curl-скрипт. Итог: агент работает автономно. Первый «настоящий» дайджест — завтра в 07:00.
Альтернативные конфигурации — тот же стек, другой профиль
Агент не DeFi-специфичен. Меняешь interests и channels — получаешь другой дайджест. Telethon, Postgres, HDBSCAN, cron — не меняются.
interests:
product:
keywords: ["feature", "roadmap", "user research", "churn", "activation", "onboarding"]
weight: 1.0
competitors:
keywords: ["Figma", "Notion", "Linear", "Jira", "competitor X"]
weight: 0.95
ai_tools:
keywords: ["LLM", "agent", "copilot", "GPT", "Claude"]
weight: 0.8
# Ключевые каналы: @lenny_newsletter, @productled, @ycombinator, @stratechery
interests:
breaking_news:
keywords: ["сенсация", "эксклюзив", "источник", "подтверждено"]
weight: 1.0
media_industry:
keywords: ["подписка", "ad revenue", "охват", "алгоритм", "reach"]
weight: 0.9
fact_checking:
keywords: ["опровержение", "уточнение", "corrected", "retracted"]
weight: 0.95
# Ключевые каналы: @meduza_live, @bbcrussian, @reuters, @ap_official
interests:
fundraising:
keywords: ["раунд", "pre-seed", "seed", "Series A", "term sheet", "valuation"]
weight: 1.0
market:
keywords: ["рынок", "TAM", "конкурент", "acquisition", "pivot"]
weight: 0.9
hiring:
keywords: ["найм", "команда", "CTO", "co-founder", "equity"]
weight: 0.75
# Ключевые каналы: @ycombinator, @techcrunch, @firstround, @sequoia
Что дальше — куда расширяется агент
- Twitter/X Watcher. Добавляем
twitter_collector.pyс Tweepy v4 или Nitter-парсером. Посты из Twitter попадают в ту же таблицуpostsсchannel_id='@twitter/handle'. Дедупликация работает одинаково — эмбеддинги не знают источника. - RSS / Newspeak-агрегатор. Добавляем
rss_collector.pyсfeedparser. Источники: Decrypt, The Block, Blockworks, CoinDesk RSS. Одна таблицаposts, один дедупликатор. Интеграция: 2–3 часа. - Weekly Report.
weekly_aggregator.pyзапускается в воскресенье в 09:00. Берёт всеdigestsза неделю, выбирает топ-10 тем по частоте упоминаний. Claude Sonnet (не Haiku — для недельного синтеза нужен масштаб) пишет 2-страничный отчёт. - Alerts (мгновенные уведомления). Telethon остаётся запущенным как слушатель (
client.on(events.NewMessage)). При появлении нового поста из vip_authors или с ключевыми словами изhigh_priority_keywords— немедленная отправка в TG (не ждать 07:00). Важно: держать alert-слушатель в отдельном процессе.
«Открываю TG только чтобы написать»
- 07:00 каждый день — четыре блока в личку: 3 важные новости + 40 фактов + мнения + вкусненькое
- В течение дня — ноль шума — Telegram не тянет тебя сам. Только ты решаешь, когда открыть
- TG открывается только чтобы написать — не листать, не «дочитать потом», не закрывать с чувством вины за 800 непрочитанных
- История дайджестов в Postgres — «что я читал в понедельник», «в каком канале был этот факт», статистика каналов за месяц
- Конфиг под тебя — profile.yaml меняется за 5 минут. Сменил нишу, добавил VIP-автора, убрал шумный канал — следующий дайджест уже другой
- Метрики деградации видны сразу — compression ratio, miss rate, retention — не надо гадать, работает ли агент
- Стек открытый — Telethon 1.43, BGE-M3, HDBSCAN, Haiku 4.5. Нет vendor lock-in. Каждый компонент заменяется независимо
Это не про продуктивность и не про дисциплину. Это про архитектуру внимания. Telegram стал push-каналом для тебя — не pull-каналом для платформы.
800 непрочитанных создают тревогу, а не ценность. Ты не пропускаешь важное, когда не читаешь их вручную — ты пропускаешь важное, когда у тебя нет агента с хорошим профилем. После настройки агент знает твои интересы лучше, чем алгоритм TG — потому что ты сам их описал.
Первые три дня будет казаться, что чего-то не хватает — рефлекс скроллинга. К концу первой недели ты перестанешь его замечать.
«Я открываю Telegram только чтобы написать кому-то.»
Основной стек:
VPS (1–2 vCPU, 2GB RAM) $5–10/мес DigitalOcean, Hetzner, Hostman
Claude Haiku 4.5 $2–6/мес $1/M input + $5/M output [verified 2026]
Postgres (включён в VPS) $0
BGE-M3 (локально) $0
Итого: $7–16/мес
Альтернативы:
├── Sonnet 4.6 вместо Haiku 4.5 ×6–8 дороже ($15–50/мес) — не нужно для дайджеста
├── OpenAI text-embedding-3-small вместо BGE-M3 $0.02/M tokens — если RAM < 1GB
├── SQLite вместо Postgres Бесплатно, но нет конкурентной записи и pgvector
└── Локальная LLM (Mistral 7B Q4) $0/мес, но нужен VPS $20+/мес с GPU или мощным CPU
Забери код агента + 2 yaml-конфига + prompt для Haiku
Подпишись на канал, напиши кодовое слово — profile.yaml, channels.yaml, Telethon-каркас, SQL-схема и prompt для Haiku 4.5 откроются в Mini App. Разворачивается на любом VPS за вечер.
Открыть в Telegram →