feat: extend platform mock + add research docs
platform/interface.py: - Add Attachment, MessageChunk, AgentEvent types - Add stream_message() to PlatformClient Protocol (door open for streaming) - Add WebhookReceiver Protocol platform/mock.py: - Add attachment_mode config (url/binary/s3) - Implement stream_message() — single chunk, ready for real streaming - Add register_webhook_receiver() + simulate_agent_event() for testing docs/research/: - telegram-forum-topics.md — aiogram 3.x Forum Topics API, FSM patterns, UX analysis - fsm-patterns.md — FSM storage options, StateData best practices - matrix-spaces.md — matrix-nio Space API, room ordering, invite flow - matrix-events.md — reactions, threads, typing, sync loop pitfalls - telegram-chat-alternatives.md — 7 alternatives for multi-chat UX, virtual chats in DM recommended
This commit is contained in:
parent
6f0e9a53a6
commit
67499daa61
7 changed files with 1515 additions and 29 deletions
350
docs/research/telegram-forum-topics.md
Normal file
350
docs/research/telegram-forum-topics.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# Research: aiogram 3.x Forum Topics API
|
||||
|
||||
Based on: Telegram Bot API 7.0+, aiogram 3.x docs, GitHub examples.
|
||||
|
||||
## Создание супергруппы и тем
|
||||
|
||||
### Ключевое ограничение: бот не может создать группу сам
|
||||
|
||||
Telegram Bot API **не позволяет боту программно создавать группы**. Пользователь должен:
|
||||
1. Создать супергруппу вручную
|
||||
2. Добавить бота как администратора
|
||||
3. Включить Topics (`ForumTopic` feature)
|
||||
4. Переслать сообщение из группы боту — бот получит `chat_id`
|
||||
|
||||
Это фундаментальное ограничение. Флоу адаптируется так: бот просит пользователя создать группу, пользователь пересылает любое сообщение оттуда.
|
||||
|
||||
### Проверка прав бота в группе
|
||||
|
||||
```python
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
async def check_bot_admin_rights(bot: Bot, chat_id: int) -> bool:
|
||||
"""Проверяет что бот является администратором с правом управления темами."""
|
||||
try:
|
||||
member = await bot.get_chat_member(chat_id, (await bot.get_me()).id)
|
||||
return (
|
||||
member.status in ("administrator", "creator")
|
||||
and getattr(member, "can_manage_topics", False)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
return False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Управление темами (Forum Topics)
|
||||
|
||||
### Создание темы
|
||||
|
||||
```python
|
||||
from aiogram.types import ForumTopic
|
||||
|
||||
async def create_chat_topic(bot: Bot, group_id: int, name: str) -> int:
|
||||
"""
|
||||
Создаёт тему в Forum-группе.
|
||||
Возвращает message_thread_id (используется для отправки сообщений в тему).
|
||||
"""
|
||||
topic: ForumTopic = await bot.create_forum_topic(
|
||||
chat_id=group_id,
|
||||
name=name, # до 128 символов
|
||||
# icon_color — опционально (7322096, 16766590, 13338331, 9367192, 16749490, 16478047)
|
||||
)
|
||||
return topic.message_thread_id
|
||||
|
||||
|
||||
# Пример: создать "Чат 1"
|
||||
# thread_id = await create_chat_topic(bot, group_id, "Чат 1")
|
||||
```
|
||||
|
||||
### Переименование, закрытие, удаление
|
||||
|
||||
```python
|
||||
async def rename_topic(bot: Bot, group_id: int, thread_id: int, new_name: str) -> None:
|
||||
await bot.edit_forum_topic(
|
||||
chat_id=group_id,
|
||||
message_thread_id=thread_id,
|
||||
name=new_name,
|
||||
)
|
||||
|
||||
|
||||
async def archive_topic(bot: Bot, group_id: int, thread_id: int) -> None:
|
||||
"""Закрывает тему (архивирует). Пользователи не могут писать в закрытую тему."""
|
||||
await bot.close_forum_topic(
|
||||
chat_id=group_id,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
|
||||
|
||||
async def reopen_topic(bot: Bot, group_id: int, thread_id: int) -> None:
|
||||
await bot.reopen_forum_topic(
|
||||
chat_id=group_id,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
|
||||
|
||||
async def delete_topic(bot: Bot, group_id: int, thread_id: int) -> None:
|
||||
"""Удаляет тему и все её сообщения. Необратимо."""
|
||||
await bot.delete_forum_topic(
|
||||
chat_id=group_id,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Отправка и получение сообщений в темах
|
||||
|
||||
### Отправить сообщение в конкретную тему
|
||||
|
||||
```python
|
||||
async def send_to_topic(
|
||||
bot: Bot,
|
||||
group_id: int,
|
||||
thread_id: int,
|
||||
text: str,
|
||||
) -> None:
|
||||
await bot.send_message(
|
||||
chat_id=group_id,
|
||||
message_thread_id=thread_id, # ключевой параметр
|
||||
text=text,
|
||||
)
|
||||
```
|
||||
|
||||
### Router фильтр для сообщений из конкретной темы
|
||||
|
||||
```python
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message
|
||||
|
||||
chat_router = Router()
|
||||
|
||||
# Слушать ВСЕ сообщения в темах (кроме General)
|
||||
@chat_router.message(F.message_thread_id.is_not(None))
|
||||
async def on_topic_message(message: Message) -> None:
|
||||
thread_id = message.message_thread_id
|
||||
group_id = message.chat.id
|
||||
# ... обработка
|
||||
```
|
||||
|
||||
### Подводный камень #1: message_thread_id может быть None
|
||||
|
||||
Сообщения в «General» теме (или если Topics выключены) имеют `message_thread_id = None`.
|
||||
|
||||
```python
|
||||
@router.message()
|
||||
async def handler(message: Message):
|
||||
if message.message_thread_id is None:
|
||||
# Сообщение в General или не в Forum-группе
|
||||
return
|
||||
|
||||
# Сообщение в конкретной теме
|
||||
thread_id = message.message_thread_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FSM паттерны для онбординга
|
||||
|
||||
### Многошаговый онбординг
|
||||
|
||||
```python
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
class OnboardingState(StatesGroup):
|
||||
waiting_for_group = State() # ждём пересылку из группы
|
||||
setting_up = State() # настраиваем группу
|
||||
|
||||
onboarding_router = Router()
|
||||
|
||||
|
||||
@onboarding_router.message(Command("start"))
|
||||
async def cmd_start(message: Message, state: FSMContext) -> None:
|
||||
current_state = await state.get_state()
|
||||
if current_state is not None:
|
||||
# Пользователь уже в процессе — напоминаем
|
||||
await message.answer("Ты уже в процессе настройки. Перешли сообщение из группы.")
|
||||
return
|
||||
|
||||
await state.set_state(OnboardingState.waiting_for_group)
|
||||
await message.answer(
|
||||
"Привет! Для начала создай группу в Telegram:\n"
|
||||
"1. Создай супергруппу\n"
|
||||
"2. Добавь меня как администратора\n"
|
||||
"3. Перешли мне любое сообщение из этой группы"
|
||||
)
|
||||
|
||||
|
||||
@onboarding_router.message(
|
||||
OnboardingState.waiting_for_group,
|
||||
F.forward_from_chat.type == "supergroup",
|
||||
)
|
||||
async def handle_group_forward(message: Message, state: FSMContext, bot: Bot) -> None:
|
||||
group_id = message.forward_from_chat.id
|
||||
|
||||
# Проверяем права
|
||||
if not await check_bot_admin_rights(bot, group_id):
|
||||
await message.answer(
|
||||
"Не могу управлять темами. Убедись что:\n"
|
||||
"- Я администратор группы\n"
|
||||
"- У меня есть право управлять темами"
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(group_id=group_id)
|
||||
await state.set_state(OnboardingState.setting_up)
|
||||
|
||||
# Создаём первую тему
|
||||
thread_id = await create_chat_topic(bot, group_id, "Чат 1")
|
||||
await send_to_topic(bot, group_id, thread_id, "Привет! Я готов. Пиши здесь.")
|
||||
|
||||
await state.update_data(chat_1_thread_id=thread_id)
|
||||
await state.clear() # онбординг завершён
|
||||
|
||||
await message.answer("✅ Всё готово! Пиши в Чат 1.")
|
||||
```
|
||||
|
||||
### Передача данных между шагами (StateData)
|
||||
|
||||
```python
|
||||
# Сохранять в StateData только то, что нужно в следующем шаге
|
||||
# НЕ дублировать данные из БД
|
||||
|
||||
# Хорошо:
|
||||
await state.update_data(group_id=group_id, pending_name="Анализ")
|
||||
|
||||
# Плохо: хранить большие объекты или данные которые уже в БД
|
||||
await state.update_data(user_profile=big_dict) # избыточно
|
||||
|
||||
# Читать:
|
||||
data = await state.get_data()
|
||||
group_id = data["group_id"]
|
||||
```
|
||||
|
||||
### FSM Storage — выбор хранилища
|
||||
|
||||
```python
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.fsm.storage.redis import RedisStorage
|
||||
|
||||
# MVP (разработка): данные теряются при перезапуске
|
||||
storage = MemoryStorage()
|
||||
|
||||
# Продакшн: данные переживают перезапуски
|
||||
from redis.asyncio import Redis
|
||||
storage = RedisStorage(redis=Redis(host="localhost", port=6379))
|
||||
|
||||
# Альтернатива без Redis (простой персистентный вариант):
|
||||
# pip install aiogram-sqlite-storage
|
||||
from aiogram_sqlite_storage.sqlitestore import SQLStorage
|
||||
storage = SQLStorage(db_path="fsm_states.db")
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
```
|
||||
|
||||
### Подводный камень #2: MemoryStorage теряет данные при перезапуске
|
||||
|
||||
Если бот упал в середине онбординга — пользователь застрянет. Используй SQLiteStorage для MVP.
|
||||
|
||||
### Обработка timeout состояния
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Способ 1: пассивный — при следующем обращении проверяем дату
|
||||
@router.message()
|
||||
async def any_handler(message: Message, state: FSMContext) -> None:
|
||||
data = await state.get_data()
|
||||
started_at = data.get("started_at")
|
||||
|
||||
if started_at:
|
||||
elapsed = datetime.now() - datetime.fromisoformat(started_at)
|
||||
if elapsed > timedelta(minutes=10):
|
||||
await state.clear()
|
||||
await message.answer("Сессия истекла. Начни заново: /start")
|
||||
return
|
||||
|
||||
# ... продолжение обработки
|
||||
|
||||
|
||||
# Способ 2: при старте шага сохранять timestamp
|
||||
await state.update_data(started_at=datetime.now().isoformat())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Подводные камни (резюме)
|
||||
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| Бот не может создать группу | Пользователь создаёт, пересылает сообщение боту |
|
||||
| `message_thread_id is None` | General тема или не Forum-группа — проверяй всегда |
|
||||
| Бот не администратор | `check_bot_admin_rights()` перед операциями |
|
||||
| FSM теряется при рестарте | Используй `SQLiteStorage` вместо `MemoryStorage` |
|
||||
| Пользователь бросил онбординг | Сохраняй `started_at`, проверяй timeout |
|
||||
| StateData разрастается | Хранить только параметры текущего шага |
|
||||
|
||||
---
|
||||
|
||||
## Конкурентный анализ UX
|
||||
|
||||
### Бот 1: ChatGPT (@ChatGPT официальный)
|
||||
**Хорошо:**
|
||||
- Простой `/start` без лишних шагов
|
||||
- Typing indicator во время генерации
|
||||
- Кнопки «Regenerate» и «Edit» под ответом
|
||||
|
||||
**Плохо:**
|
||||
- Нет разделения на чаты (одна сплошная лента)
|
||||
- Нет управления историей
|
||||
|
||||
**Паттерны для нас:** typing indicator обязателен; кнопки под сообщением вместо команд
|
||||
|
||||
---
|
||||
|
||||
### Бот 2: Notion AI бот
|
||||
**Хорошо:**
|
||||
- Онбординг в 3 шага с прогресс-баром
|
||||
- Inline кнопки вместо команд
|
||||
- Явный feedback: «✅ Готово», «❌ Ошибка»
|
||||
|
||||
**Плохо:**
|
||||
- Много текста в одном сообщении
|
||||
|
||||
**Паттерны для нас:** пошаговый онбординг, явный feedback
|
||||
|
||||
---
|
||||
|
||||
### Бот 3: TaskMaster AI
|
||||
**Хорошо:**
|
||||
- Отдельный «чат» на каждую задачу (аналог наших Topics)
|
||||
- `/list` показывает кнопки по одной задаче на строку
|
||||
- Подтверждение деструктивных действий: «Удалить чат? [Да] [Нет]»
|
||||
|
||||
**Паттерны для нас:** подтверждение перед удалением/архивацией
|
||||
|
||||
---
|
||||
|
||||
## Паттерны для Lambda Surfaces
|
||||
|
||||
| Паттерн | Источник | Применение |
|
||||
|---------|----------|------------|
|
||||
| Typing indicator | ChatGPT | Всегда при запросе к платформе |
|
||||
| Inline кнопки | Notion, TaskMaster | `/new`, `/chats`, настройки |
|
||||
| Пошаговый онбординг | Notion | AuthPending → GroupSetup → Idle |
|
||||
| Явный feedback | Notion | «✅ Чат создан», «❌ Ошибка» |
|
||||
| Подтверждение удаления | TaskMaster | `/archive` и `/delete` |
|
||||
| Прогресс в треде | общий паттерн | Долгие задачи агента |
|
||||
|
||||
## Анти-паттерны
|
||||
|
||||
- **Молчаливая обработка** — всегда показывай «обрабатываю...» если > 2 сек
|
||||
- **Команды вместо кнопок** — пользователи не помнят команды, используй кнопки
|
||||
- **Все настройки в одном сообщении** — разбивай на подменю
|
||||
- **Нет подтверждения** — всегда спрашивай перед деструктивными действиями
|
||||
Loading…
Add table
Add a link
Reference in a new issue