Библиотека
📬
Получить материал в Telegram Подпишись на канал и заполни короткую анкету — конфиги откроются в нашем приложении
Открыть в Telegram →
Гайд · AI-стек · TG-агент

800 непрочитанных в TG: дайджест за тебя

Агент читает 80+ каналов и присылает структурированную сводку каждое утро в 07:00. Четыре блока: Top-3 + дедупликация + мнения + вкусненькое. Полный стек, конфиги и код — внутри.

Формат: Архитектура + конфиг + код Время: ~30–40 минут на чтение Стек: Claude Code · Telethon 1.43 · Postgres · sentence-transformers (BGE-M3) Бюджет: ~$8–18/мес (VPS + Claude Haiku 4.5)

Что внутри гайда

1

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 — три прохода

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 предложений из первоисточника (дословно, в кавычках).

НЕ придумывай факты. Если цифры нет в постах — не пиши цифру.
НЕ начинай абзац со слова "Итак" или "Таким образом".
Пример вывода в TG:
🔴 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.»
2

40 фактов вместо 800 постов

Дедупликация

800 непрочитанных — это не 800 разных новостей. Это одна новость, пересказанная в 15 каналах, плюс 200 постов воды, рекламы и «коллеги, ловите важное». Блок 2 схлопывает всё это в 40 фактов.

Дедупликация — это не просто «удали дубли». Это сжатие информационного пространства с сохранением лучшего представителя каждой темы. Три этапа: эмбеддинги → кластеризация → выбор представителя.

Выбор модели — BGE-M3 [verified 2026]

Рекомендуемая модель: BAAI/bge-m3 (sentence-transformers). Почему именно она, а не paraphrase-multilingual-MiniLM-L12-v2 (упомянутая в v1 brief):

Пайплайн дедупликации — 4 шага

  1. Embed. Каждый пост прогоняется через bge-m3. Батчи по 32 поста. Результат — вектор float32[1024].
  2. Cluster. HDBSCAN с min_cluster_size=2, min_samples=1, metric='cosine'. Порог эффективно задаётся через cluster_selection_epsilon=0.15 (= 1 − cosine_threshold 0.85).
  3. Select representative. Из каждого кластера выбирается пост от канала с наибольшим weight в channels.yaml. При равных весах — более длинный пост.
  4. Filter noise. Посты, помеченные HDBSCAN как шум (label=-1), прогоняются через Haiku 4.5: «это факт/аргумент с данными или вода?». Оставляем is_signal=true.

Почему HDBSCAN, а не агломеративная кластеризация

Порог cosine similarity — почему именно 0.85

В 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
Типичные ловушки дедупликации:
  1. Каскадные ссылки на один источник. Десять каналов пересылают один пост из @криптопанорамы с пометкой «важно» — тексты почти идентичны. BGE-M3 объединяет их в один кластер. Представитель — оригинал (наибольший вес). Проблема: если @криптопанорама нет в твоём channels.yaml — её вес = 0, представителем станет пересыльщик. Решение: добавить базовые новостные каналы в channels.yaml даже с низким весом.
  2. Один автор — несколько постов на одну тему. Если VIP-автор написал тред из 5 постов — они попадут в один кластер. Только первый станет представителем. Добавь пост-агрегацию для vip_authors: конкатенация всех постов одного автора за ночь перед эмбеддингом.
  3. Короткие посты (<80 символов). Сообщения вида «ETH $3200» дают нестабильные эмбеддинги. Стоп-список min_length_chars: 80 в profile.yaml решает это до кластеризации.
3

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 — как агент собирает блок «Мнения»

Prompt-template для секции «Мнения»

SYSTEM:
Ты — редактор раздела «Мнения» в дайджесте. Задача: выжать суть из постов
авторитетного автора за ночь. Язык — русский. Максимальная длина на автора: 3 предложения.

USER:
Автор: {{author_name}} ({{author_handle}})
Число постов за ночь: {{post_count}}

ПОСТЫ (хронологически):
{{posts_block}}

Напиши в формате:
**{{author_name}}** — [одно предложение: о чём написал за ночь].
Цитата: «{{дословная цитата ≤ 30 слов из наиболее важного поста}}»
→ [ссылка на оригинал]

Если постов не было — верни пустую строку (не пиши "не написал").
Если посты на разные темы — выбери наиболее резонансную для DeFi/крипто/бизнеса.
Как выбрать кого слушать — критерии для vip_authors:
  • Ты читаешь вручную, когда замечаешь новый пост
  • Автор даёт оценку/позицию, а не только пересказывает
  • Пишет реже чем 5 раз в день (иначе засорит блок 3)
  • Опыт в твоей нише > 3 лет
  • Начни с 3 авторов. Максимум — 5. Больше = блок 3 теряет ценность.
4

Не критично, но интересно

Вкусненькое — если есть время

Не всё попадает в Top-3. Есть длинные эссе, нестандартные ниши, идеи из смежных отраслей — то, что не горит, но мозг скажет спасибо. Блок 4 — это «полка для чтения в самолёте».

В блок 4 идут посты, которые прошли фильтр воды (реальный факт/аргумент), но набрали недостаточно кросс-упоминаний для Top-3 и не являются VIP-мнением. Лимит: не больше 7 пунктов за раз.

Критерии — что попадает в блок 4

Haiku 4.5 помечает тип контента для блока 4:

Формат блока 4 в дайджесте: список с тегом типа, заголовком в 10 слов и ссылкой. Без суммаризации — только навигация. Читать или нет — твоё усмотрение.

Лимиты

Архитектурный смысл блока 4. Без блока 4 агент режет всё, что не попало в топ, и ты теряешь случайные находки. Практика: раз в неделю из блока 4 приходит идея, которая стоит больше всего Top-3 за ту же неделю. Это «случайное открытие» — и оно не случайное, если ты правильно настроил adjacent интересы.
5

Telethon + Postgres + Claude Haiku 4.5 + cron

~$8–18/мес полный стек

Агент работает 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() — реальный сценарий, не теоретический.
6

7 граблей с боевого использования

Предупреждён — вооружён

Агент не сломается на первый день. Но через неделю ты встретишь их всех. Вот полный список граблей с симптомами и фиксами.

Ни один из этих сценариев не описан в официальной документации Telegram или Telethon. Всё ниже — боевой опыт.

  1. Spam-фильтр (нежелательная реклама в дайджесте).
    Симптом: в блок 2 «фактов» попадают посты вида «Этот протокол даёт 40% годовых, регистрируйся по реферальной ссылке».
    Причина: рекламные посты содержат реальные цифры — фильтр воды (Haiku) ошибается.
    Фикс: добавить URL-паттерн в стоп-список (?ref=, utm_source, t.me/+invite). Отдельным шагом до эмбеддинга проверять наличие гиперссылок в тексте — если ссылка есть в >60% схожих постов от нового канала, снижать вес канала автоматически.
  2. Session revoke (сессия Telethon умерла).
    Симптом: AuthKeyUnregisteredError или SessionPasswordNeededError в логах. Дайджест не приходит.
    Причина: (а) ты залогинился в Telegram на новом устройстве и приложение сбросило старые сессии; (б) вызов set_dc() на активной сессии [КРИТИЧНО]; (в) аккаунт был временно ограничен Telegram.
    Фикс: мониторинг через health_check.sh. При AuthKeyError — отправить TG-уведомление тебе через резервный бот-токен. Реавторизация вручную занимает 2 минуты — главное вовремя узнать.
  3. 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) между каналами (уже в скрипте выше). При FloodWaitErrorawait asyncio.sleep(e.seconds + 5) и продолжать со следующего канала.
  4. Длина сообщения 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) — читать удобнее.
  5. Мульти-аккаунт и параллельные сессии.
    Симптом: если запустить два экземпляра коллектора одновременно — AuthKeyDuplicatedError.
    Причина: Telethon не поддерживает параллельные подключения с одним SESSION-файлом.
    Фикс: lockfile перед стартом коллектора: fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB). Если блокировка не получена — завершаем процесс.
  6. Проблемы с кодировкой (encoding errors).
    Симптом: UnicodeDecodeError при записи в Postgres или при обработке текста.
    Причина: TG-посты содержат emoji (4-байтовые символы Unicode), экзотические символы, нулевые байты.
    Фикс: при вставке в Postgres: text.replace('\x00', ''). Убедись, что БД создана с ENCODING='UTF8' и LC_COLLATE='en_US.UTF-8'.
  7. Каналы без истории (restricted history).
    Симптом: iter_messages возвращает 0 постов для некоторых каналов.
    Причина: часть каналов включает ограничение «показывать историю только участникам последних X дней». Telethon-сессия (user-аккаунт) может не иметь доступа к старым постам.
    Фикс: добавить флаг accessible: false при первой попытке iter_messages вернуть 0 — и использовать fallback-канал из channels.yaml. Либо подключить Telethon-сессию аккаунта, который давно подписан на эти каналы.
Главное правило операционки: health_check.sh должен запускаться отдельным cron в 07:05 — если за сегодня нет записи в digests, шлём тебе TG-алерт через резервный бот (отдельный токен, отдельный аккаунт). Так ты узнаешь о поломке через 5 минут после того, как должен был получить дайджест.
7

Как замерить, что дайджест работает

Цифры, а не ощущения

«Дайджест хороший» — это не метрика. Метрика — сколько важных событий ты пропустил и сколько времени потратил на дайджест вместо скроллинга.

Без измерений агент деградирует: промпты ломаются, каналы меняют тематику, пороги перестают работать. Эти 6 метрик позволяют заметить деградацию до того, как она стала проблемой.

6 метрик качества

Бенчмарки — что считать «хорошим» дайджестом

Когда мерить. Первая неделя — установочная, цифры мерить бессмысленно. Со второй недели запускай weekly health: cron в воскресенье в 20:00 считает все 6 метрик за 7 дней и шлёт тебе одной строкой в TG. Если 2+ метрики ушли из «нормы» — переоткрой profile.yaml.
8

От нуля до первого дайджеста — 7 шагов

Чек-лист на вечер

Агент разворачивается за вечер (3–4 часа). Вот последовательность, которая не ломается.

  1. Шаг 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. Шаг 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. Шаг 3 — Заполнить profile.yaml и channels.yaml (30 мин).
    Скопируй примеры из section 1 и section 3 этого гайда. Начни с 10–15 каналов (можно расширить позже). Укажи 2–3 VIP-авторов (не больше на старте). Заполни interests честно — не «всё интересно», а что реально влияет на решения.
  4. Шаг 4 — Авторизовать Telethon-сессию (5 мин).
    Запусти python digest_collector.py вручную первый раз. Telethon запросит номер телефона и код из SMS/приложения. После успешной авторизации появится digest_session.session — это твой токен, храни как секрет. НИКОГДА не перемещай этот файл на другой сервер без реавторизации.
  5. Шаг 5 — Тестовый запуск коллектора (15 мин).
    Запусти digest_collector.py вручную, проверь в Postgres: SELECT COUNT(*) FROM posts;. Должно появиться 50–500 постов в зависимости от активности каналов. Если 0: проверь, что аккаунт подписан на указанные каналы вручную в Telegram.
  6. Шаг 6 — Запустить агрегатор и проверить дайджест (30 мин).
    Запусти digest_aggregator.py вручную (кластеризация + Claude Haiku 4.5). Проверь вывод: три блока текста, ссылки рабочие, нет hallucinated фактов. Настрой anthropic клиент: pip install anthropic, ANTHROPIC_API_KEY=.... Если качество плохое — пересмотри interests в profile.yaml (самая частая причина).
  7. Шаг 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 — не меняются.

Для Product Manager:
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
Для стартапера (pre-seed / seed):
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

Что дальше — куда расширяется агент

«Открываю TG только чтобы написать»

Твой Telegram утром

Это не про продуктивность и не про дисциплину. Это про архитектуру внимания. Telegram стал push-каналом для тебя — не pull-каналом для платформы.

800 непрочитанных создают тревогу, а не ценность. Ты не пропускаешь важное, когда не читаешь их вручную — ты пропускаешь важное, когда у тебя нет агента с хорошим профилем. После настройки агент знает твои интересы лучше, чем алгоритм TG — потому что ты сам их описал.

Первые три дня будет казаться, что чего-то не хватает — рефлекс скроллинга. К концу первой недели ты перестанешь его замечать.

«Я открываю Telegram только чтобы написать кому-то.»

Бюджет $8–18/мес — полный breakdown:
Основной стек:
  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 →
КОДОВОЕ СЛОВО · ДАЙДЖЕСТ