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
264
docs/research/matrix-spaces.md
Normal file
264
docs/research/matrix-spaces.md
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
# Research: matrix-nio Space API
|
||||
|
||||
Based on: Matrix Client-Server Spec, matrix-nio 0.24+, Element Web behavior.
|
||||
|
||||
## Создание Space
|
||||
|
||||
Space в Matrix — это комната с room type `m.space`. При создании нужны state events.
|
||||
|
||||
### Полный рабочий пример
|
||||
|
||||
```python
|
||||
from nio import AsyncClient, RoomCreateResponse
|
||||
|
||||
async def create_personal_space(client: AsyncClient, display_name: str) -> str:
|
||||
"""
|
||||
Создаёт персональный Space для пользователя.
|
||||
Возвращает room_id созданного Space.
|
||||
"""
|
||||
response = await client.room_create(
|
||||
name=f"Lambda — {display_name}",
|
||||
is_public=False,
|
||||
room_version="10",
|
||||
initial_state=[
|
||||
{
|
||||
"type": "m.room.topic",
|
||||
"state_key": "",
|
||||
"content": {"topic": "Lambda AI workspace"},
|
||||
},
|
||||
{
|
||||
"type": "m.room.join_rules",
|
||||
"state_key": "",
|
||||
"content": {"join_rule": "invite"},
|
||||
},
|
||||
],
|
||||
creation_content={
|
||||
"type": "m.space", # ЭТО ГЛАВНОЕ — иначе создастся обычная комната
|
||||
},
|
||||
)
|
||||
|
||||
if isinstance(response, RoomCreateResponse):
|
||||
return response.room_id
|
||||
raise Exception(f"Failed to create space: {response}")
|
||||
```
|
||||
|
||||
### Подводный камень #1: creation_content обязателен
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО — создаст обычную комнату, не Space
|
||||
await client.room_create(name="My Space")
|
||||
|
||||
# ПРАВИЛЬНО
|
||||
await client.room_create(
|
||||
name="My Space",
|
||||
creation_content={"type": "m.space"},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Добавление комнат в Space
|
||||
|
||||
Чтобы добавить комнату в Space, нужно установить state event `m.space.child` в самом Space.
|
||||
|
||||
### Создание комнаты и добавление в Space
|
||||
|
||||
```python
|
||||
async def create_room_in_space(
|
||||
client: AsyncClient,
|
||||
space_room_id: str,
|
||||
room_name: str,
|
||||
order: str,
|
||||
) -> str:
|
||||
"""
|
||||
Создаёт комнату и добавляет её в Space.
|
||||
Возвращает room_id новой комнаты.
|
||||
"""
|
||||
# 1. Создаём комнату
|
||||
room_resp = await client.room_create(
|
||||
name=room_name,
|
||||
is_public=False,
|
||||
room_version="10",
|
||||
initial_state=[
|
||||
{
|
||||
"type": "m.room.join_rules",
|
||||
"state_key": "",
|
||||
"content": {"join_rule": "invite"},
|
||||
},
|
||||
],
|
||||
)
|
||||
if not isinstance(room_resp, RoomCreateResponse):
|
||||
raise Exception(f"Failed to create room: {room_resp}")
|
||||
room_id = room_resp.room_id
|
||||
|
||||
# 2. Добавляем в Space через m.space.child
|
||||
# state_key = room_id дочерней комнаты (НЕ произвольное имя!)
|
||||
await client.room_put_state(
|
||||
room_id=space_room_id,
|
||||
event_type="m.space.child",
|
||||
state_key=room_id,
|
||||
content={
|
||||
"via": ["example.com"], # серверы для присоединения — обязательно
|
||||
"order": order, # строковый порядок
|
||||
"suggested": True,
|
||||
},
|
||||
)
|
||||
|
||||
# 3. Обратная ссылка (помогает клиентам понять структуру)
|
||||
await client.room_put_state(
|
||||
room_id=room_id,
|
||||
event_type="m.space.parent",
|
||||
state_key=space_room_id,
|
||||
content={
|
||||
"via": ["example.com"],
|
||||
"canonical": True,
|
||||
},
|
||||
)
|
||||
|
||||
return room_id
|
||||
```
|
||||
|
||||
### Подводный камень #2: state_key — это room_id, не имя
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО
|
||||
await client.room_put_state(
|
||||
room_id=space_id,
|
||||
event_type="m.space.child",
|
||||
state_key="Настройки", # ОШИБКА: нужен room_id
|
||||
content={"via": ["example.com"]},
|
||||
)
|
||||
|
||||
# ПРАВИЛЬНО
|
||||
await client.room_put_state(
|
||||
room_id=space_id,
|
||||
event_type="m.space.child",
|
||||
state_key="!abc123:example.com", # именно room_id
|
||||
content={"via": ["example.com"]},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Управление порядком комнат
|
||||
|
||||
Порядок в Space контролируется полем `order` в `m.space.child`. Это строковое сравнение.
|
||||
|
||||
### Закрепить «Настройки» вверху, чаты ниже
|
||||
|
||||
```python
|
||||
async def setup_space_order(
|
||||
client: AsyncClient,
|
||||
space_id: str,
|
||||
settings_room_id: str,
|
||||
chat_room_ids: list[str],
|
||||
) -> None:
|
||||
# Настройки вверху
|
||||
await client.room_put_state(
|
||||
room_id=space_id,
|
||||
event_type="m.space.child",
|
||||
state_key=settings_room_id,
|
||||
content={"via": ["example.com"], "order": "00", "suggested": True},
|
||||
)
|
||||
|
||||
# Чаты с нарастающим порядком
|
||||
for idx, chat_id in enumerate(chat_room_ids):
|
||||
await client.room_put_state(
|
||||
room_id=space_id,
|
||||
event_type="m.space.child",
|
||||
state_key=chat_id,
|
||||
content={"via": ["example.com"], "order": f"{10 + idx:02d}", "suggested": True},
|
||||
)
|
||||
```
|
||||
|
||||
### Подводный камень #3: order — строковое, нужен padding
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО: "10" < "2" в строковом сравнении!
|
||||
orders = ["0", "1", "10", "11", "2", "3"]
|
||||
|
||||
# ПРАВИЛЬНО: одинаковая длина
|
||||
orders = ["00", "01", "02", "03", "10", "11"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Приглашение пользователя в Space и дочерние комнаты
|
||||
|
||||
Простого способа пригласить в Space и все дочерние комнаты одним вызовом нет — нужно приглашать в каждую отдельно.
|
||||
|
||||
```python
|
||||
async def invite_user_to_space_and_rooms(
|
||||
client: AsyncClient,
|
||||
space_id: str,
|
||||
child_room_ids: list[str],
|
||||
user_id: str,
|
||||
) -> None:
|
||||
# Приглашаем в Space
|
||||
await client.room_invite(space_id, user_id)
|
||||
|
||||
# Приглашаем в каждую дочернюю комнату
|
||||
for room_id in child_room_ids:
|
||||
await client.room_invite(room_id, user_id)
|
||||
```
|
||||
|
||||
### Подводный камень #4: пользователь в Space не видит комнаты автоматически
|
||||
|
||||
Приглашение в Space не тянет дочерние комнаты — нужно приглашать каждую явно.
|
||||
|
||||
---
|
||||
|
||||
## Переименование и удаление из Space
|
||||
|
||||
### Переименование комнаты
|
||||
|
||||
```python
|
||||
async def rename_room(client: AsyncClient, room_id: str, new_name: str) -> None:
|
||||
await client.room_put_state(
|
||||
room_id=room_id,
|
||||
event_type="m.room.name",
|
||||
state_key="",
|
||||
content={"name": new_name},
|
||||
)
|
||||
```
|
||||
|
||||
### Удаление комнаты из Space (без удаления самой комнаты)
|
||||
|
||||
```python
|
||||
async def remove_from_space(
|
||||
client: AsyncClient,
|
||||
space_id: str,
|
||||
room_id: str,
|
||||
) -> None:
|
||||
# Пустой content = убрать из Space
|
||||
await client.room_put_state(
|
||||
room_id=space_id,
|
||||
event_type="m.space.child",
|
||||
state_key=room_id,
|
||||
content={}, # пустое — убирает комнату из Space
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Подводные камни (резюме)
|
||||
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| Space создаётся как обычная комната | Используй `creation_content={"type": "m.space"}` |
|
||||
| Комнаты не видны в Space | Добавь `m.space.child` с `state_key=room_id` |
|
||||
| Неправильный порядок | Строковый padding: `"00"`, `"01"`, `"10"` |
|
||||
| Пользователь не видит комнаты | Приглашай в каждую дочернюю отдельно |
|
||||
| Комната не убирается из Space | Установи пустой `content: {}` в `m.space.child` |
|
||||
| `via` не указан | Всегда указывай `"via": ["homeserver.com"]` |
|
||||
|
||||
---
|
||||
|
||||
## Выводы для нашей реализации
|
||||
|
||||
1. **При регистрации**: `create_personal_space()` → сохранить `space_id` в БД
|
||||
2. **Комната «Настройки»**: создать первой с `order: "00"`
|
||||
3. **«Чат 1»**: создать второй с `order: "10"`
|
||||
4. **Команда `!new`**: создать комнату + `m.space.child` + пригласить пользователя
|
||||
5. **Команда `!rename`**: обновить `m.room.name`
|
||||
6. **Команда `!archive`**: установить пустой `m.space.child`
|
||||
Loading…
Add table
Add a link
Reference in a new issue