surfaces/docs/superpowers/specs/2026-03-31-telegram-adapter-design.md

8.4 KiB
Raw Blame History

Telegram Adapter Design

Date: 2026-03-31 Status: Approved — implemented in feat/telegram-adapter Scope: adapter/telegram/


Контекст

Telegram-адаптер — поверхность для взаимодействия пользователя с AI-агентом Lambda через Telegram. Адаптер конвертирует Telegram-события в IncomingEvent (core protocol) и отправляет OutgoingEvent обратно. Бизнес-логика остаётся в core/; адаптер отвечает за Telegram API, FSM и локальную SQLite-привязку Telegram-пользователя и чатов.


Чаты: hybrid DM + Forum Topics

Зафиксированное решение: базовая поверхность — личка с ботом, Forum Topics — опциональный advanced-режим поверх тех же самых чатов.

  • В DM пользователь всегда может писать сразу после /start
  • active_chat_id хранится в FSM и определяет, в какой чат идут DM-сообщения
  • Если подключена Forum-группа, каждый чат может получить forum_thread_id
  • Один и тот же chat_id доступен из двух поверхностей:
    • DM: ответы идут с префиксом [Название чата]
    • Forum-тема: ответы идут прямо в тему без префикса

UX флоу

/start
→ пользователь аутентифицирован
→ создаётся или восстанавливается активный DM-чат

/new [название] в DM
→ создаётся новый чат
→ если forum уже подключён, бот создаёт и forum topic

/forum
→ бот просит переслать сообщение из супергруппы с Topics
→ проверяет admin rights
→ привязывает группу к пользователю
→ создаёт topics для существующих чатов

Сообщение в DM
→ идёт в active_chat_id
→ ответ приходит в DM как `[Чат #N] ...`

Сообщение в forum topic
→ по `message_thread_id` определяется chat_id
→ ответ приходит в ту же тему без тега

Аутентификация

Флоу (мок)

  1. /startplatform.get_or_create_user(external_id=tg_user_id, platform="telegram", display_name=...)
  2. Бот сохраняет tg_user_id -> platform_user_id в локальной БД
  3. Если локальных чатов ещё нет — создаёт Чат #1
  4. Если чат уже есть — восстанавливает последний активный чат

FSM состояния

class ChatState(StatesGroup):
    idle = State()
    waiting_response = State()


class SettingsState(StatesGroup):
    menu = State()
    soul_editing = State()
    confirm_action = State()


class ForumSetupState(StatesGroup):
    waiting_for_group = State()

active_chat_id и active_chat_name хранятся в FSMContext data.


Структура файлов

adapter/telegram/
  bot.py                  — точка входа: Dispatcher, middleware, routers
  converter.py            — Message -> IncomingMessage, forum helpers, output formatting
  db.py                   — SQLite schema и Telegram-specific persistence
  states.py               — ChatState, SettingsState, ForumSetupState

  handlers/
    auth.py               — /start
    chat.py               — /new, /chats, switch chat, входящие сообщения
    confirm.py            — confirm/cancel callbacks
    forum.py              — /forum onboarding и регистрация forum group
    settings.py           — /settings и callbacks настроек

  keyboards/
    chat.py               — список чатов
    confirm.py            — confirm keyboard
    settings.py           — меню настроек

Persistence

Локальная БД содержит две Telegram-специфичные сущности:

CREATE TABLE tg_users (
    tg_user_id       INTEGER PRIMARY KEY,
    platform_user_id TEXT NOT NULL,
    display_name     TEXT,
    created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    forum_group_id   INTEGER
);

CREATE TABLE chats (
    chat_id         TEXT PRIMARY KEY,
    tg_user_id      INTEGER NOT NULL,
    name            TEXT NOT NULL,
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    archived_at     TIMESTAMP,
    forum_thread_id INTEGER
);
  • forum_group_id — привязанная супергруппа пользователя
  • forum_thread_id — опциональная связь конкретного чата с forum topic

Converter

Telegram -> IncomingEvent

def from_message(message: Message, chat_id: str) -> IncomingMessage:
    return IncomingMessage(
        user_id=str(message.from_user.id),
        chat_id=chat_id,
        text=message.text or message.caption or "",
        attachments=_extract_attachments(message),
        platform="telegram",
    )


def is_forum_message(message: Message) -> bool:
    return getattr(message, "message_thread_id", None) is not None


def resolve_forum_chat_id(message: Message, tg_user_id: int) -> str | None:
    thread_id = getattr(message, "message_thread_id", None)
    if thread_id is None:
        return None

    chat = db.get_chat_by_thread(tg_user_id, thread_id)
    return chat["chat_id"] if chat else None

OutgoingEvent -> Telegram

def format_outgoing(chat_name: str, event: OutgoingEvent, *, prefix: bool = True) -> str:
    rendered_prefix = f"[{chat_name}] " if prefix else ""
    return rendered_prefix + event.text
  • DM-ответы используют prefix=True
  • Forum-ответы используют prefix=False
  • OutgoingUI отправляется с inline-кнопками подтверждения

Обработчики

auth.py

  • /start создаёт или восстанавливает пользователя
  • если это первый запуск, создаёт Чат #1
  • обновляет active_chat_id и переводит FSM в ChatState.idle

chat.py

  • /new:
    • в DM создаёт новый чат
    • если подключён forum, пытается создать forum topic и сохранить forum_thread_id
    • в forum-теме может зарегистрировать текущую тему как чат
  • /chats показывает inline-список чатов
  • switch:<chat_id>:<name> переключает активный DM-чат
  • handle_message:
    • в DM читает active_chat_id из FSM
    • в forum определяет чат по message_thread_id
    • отправляет typing
    • прокидывает IncomingMessage в EventDispatcher
    • возвращает ответ в DM или в тему

forum.py

  • /forum переводит FSM в ForumSetupState.waiting_for_group
  • пересланное сообщение из супергруппы:
    • валидирует, что это supergroup
    • проверяет, что бот admin и умеет can_manage_topics
    • сохраняет forum_group_id
    • создаёт topics для существующих чатов без forum_thread_id

confirm.py

  • обрабатывает confirm:yes:<action_id> и confirm:no:<action_id>
  • в forum-режиме восстанавливает chat_id по thread
  • ответ на callback отправляет обратно в тот же канал:
    • DM -> в личку
    • Forum -> в тот же message_thread_id

Текущее покрытие

  • unit-тесты на forum routing и forum onboarding: tests/adapter/telegram/test_forum.py
  • smoke/integration на dispatcher и core handlers:
    • tests/core/test_dispatcher.py
    • tests/core/test_integration.py

Что не покрывает этот документ

  • Matrix-адаптер
  • Реальный SDK платформы вместо sdk.mock.MockPlatformClient
  • Автоматическое отслеживание вручную созданных пользователем forum topics без /new