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
129
docs/research/fsm-patterns.md
Normal file
129
docs/research/fsm-patterns.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Research: FSM паттерны в aiogram 3.x
|
||||
|
||||
## Хранилище состояний (Storage)
|
||||
|
||||
### Выбор по фазе разработки
|
||||
|
||||
| Фаза | Storage | Плюсы | Минусы |
|
||||
|------|---------|-------|--------|
|
||||
| Разработка | `MemoryStorage` | Нет зависимостей | Теряется при рестарте |
|
||||
| MVP | `SQLiteStorage` | Персистентен, нет Redis | Не распределённый |
|
||||
| Продакшн | `RedisStorage` | Распределённый, быстрый | Нужен Redis |
|
||||
|
||||
```python
|
||||
# Разработка
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
storage = MemoryStorage()
|
||||
|
||||
# MVP — pip install aiogram-sqlite-storage
|
||||
from aiogram_sqlite_storage.sqlitestore import SQLStorage
|
||||
storage = SQLStorage(db_path="fsm.db")
|
||||
|
||||
# Продакшн
|
||||
from aiogram.fsm.storage.redis import RedisStorage
|
||||
from redis.asyncio import Redis
|
||||
storage = RedisStorage(redis=Redis(host="localhost"))
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура StateData
|
||||
|
||||
### Правило: хранить только параметры текущего шага
|
||||
|
||||
```python
|
||||
# ПРАВИЛЬНО: только то что нужно в следующем шаге
|
||||
await state.update_data(
|
||||
group_id=group_id, # ID группы для следующего шага
|
||||
pending_name="Чат 2", # временное имя до подтверждения
|
||||
)
|
||||
|
||||
# НЕПРАВИЛЬНО: дублировать данные из БД
|
||||
await state.update_data(
|
||||
user_name="Иван", # уже есть в БД
|
||||
all_chats=[...], # большой объект, лучше запрашивать из БД
|
||||
)
|
||||
```
|
||||
|
||||
### Типичная StateData для онбординга
|
||||
|
||||
```python
|
||||
# Шаг 1: узнали group_id
|
||||
await state.update_data(group_id=123456789, started_at=datetime.now().isoformat())
|
||||
|
||||
# Шаг 2: узнали имя пользователя
|
||||
await state.update_data(display_name="Иван")
|
||||
|
||||
# Финал: читаем всё
|
||||
data = await state.get_data()
|
||||
group_id = data["group_id"]
|
||||
display_name = data.get("display_name", "Пользователь")
|
||||
await state.clear() # очистить после завершения
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Кастомные фильтры
|
||||
|
||||
```python
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
class IsForumTopicFilter(BaseFilter):
|
||||
"""True если сообщение отправлено в тему Forum-группы."""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return message.message_thread_id is not None
|
||||
|
||||
|
||||
class IsGroupAdminFilter(BaseFilter):
|
||||
"""True если пользователь является администратором группы."""
|
||||
async def __call__(self, message: Message, bot) -> bool:
|
||||
if message.chat.type not in ("group", "supergroup"):
|
||||
return False
|
||||
member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
return member.status in ("administrator", "creator")
|
||||
|
||||
|
||||
# Использование:
|
||||
@router.message(IsForumTopicFilter())
|
||||
async def on_topic_message(message: Message): ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обработка ошибок в FSM
|
||||
|
||||
```python
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message, ErrorEvent
|
||||
|
||||
error_router = Router()
|
||||
|
||||
@error_router.error()
|
||||
async def handle_error(event: ErrorEvent) -> None:
|
||||
"""Глобальный обработчик ошибок."""
|
||||
update = event.update
|
||||
exception = event.exception
|
||||
|
||||
# Логируем
|
||||
import logging
|
||||
logging.error(f"Error handling update: {exception}", exc_info=True)
|
||||
|
||||
# Пытаемся уведомить пользователя
|
||||
if update.message:
|
||||
await update.message.answer(
|
||||
"Что-то пошло не так. Попробуй ещё раз или напиши /start"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Выводы для нашей реализации
|
||||
|
||||
1. **MVP**: `SQLiteStorage` — персистентен, нет зависимостей от Redis
|
||||
2. **StateData**: хранить только `group_id`, `thread_id` текущей операции
|
||||
3. **Timeout**: сохранять `started_at`, проверять при каждом шаге
|
||||
4. **Очищать state**: `await state.clear()` после завершения онбординга
|
||||
5. **Ошибки**: глобальный `@error_router.error()` для graceful degradation
|
||||
Loading…
Add table
Add a link
Reference in a new issue