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

236 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 флоу
```text
/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. `/start``platform.get_or_create_user(external_id=tg_user_id, platform="telegram", display_name=...)`
2. Бот сохраняет `tg_user_id -> platform_user_id` в локальной БД
3. Если локальных чатов ещё нет — создаёт `Чат #1`
4. Если чат уже есть — восстанавливает последний активный чат
### FSM состояния
```python
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.
---
## Структура файлов
```text
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-специфичные сущности:
```sql
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
```python
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
```python
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`