init: surfaces-bot — Telegram & Matrix prototype
- Surface Protocol: unified IncomingMessage/OutgoingUI/ChatContext - Telegram: Forum Topics (group + topics per chat) - Matrix: Space + rooms per chat - MockPlatformClient with PlatformClient Protocol - docs: surface-protocol, telegram/matrix specs, api-contract, claude-code-guide - project scaffold: src/, tests/, pyproject.toml Co-Authored-By: Claude Sonnet 4-6 <noreply@anthropic.com>
This commit is contained in:
commit
b6df29bd9b
29 changed files with 2504 additions and 0 deletions
311
docs/surface-protocol.md
Normal file
311
docs/surface-protocol.md
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# Surface Protocol — унификация поверхностей
|
||||
|
||||
## Идея
|
||||
|
||||
Любая поверхность (Telegram, Matrix, будущий Discord, голос, web) делает одно
|
||||
и то же: принимает события от пользователя и отправляет ответы обратно.
|
||||
|
||||
Разница только в том как событие выглядит снаружи: aiogram `Message`,
|
||||
matrix-nio `RoomMessageText`, Discord `Interaction`. Внутри — одно и то же.
|
||||
|
||||
Surface Protocol — это общий язык между адаптерами поверхностей и ядром.
|
||||
Адаптер конвертирует нативные события в протокольные структуры и обратно.
|
||||
Ядро работает только с протокольными структурами и ничего не знает о транспорте.
|
||||
|
||||
**Добавить новую поверхность = написать один адаптер-конвертер.**
|
||||
Ядро не трогается.
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
surfaces-bot/
|
||||
core/
|
||||
protocol.py — все унифицированные структуры данных
|
||||
handler.py — логика: IncomingEvent → OutgoingEvent
|
||||
session.py — управление сессиями и чатами
|
||||
auth.py — AuthFlow
|
||||
settings.py — SettingsAction, управление настройками
|
||||
|
||||
adapter/
|
||||
telegram/ — aiogram адаптер
|
||||
converter.py — aiogram Event → IncomingEvent, OutgoingEvent → aiogram API
|
||||
bot.py — точка входа, роутер
|
||||
matrix/ — matrix-nio адаптер
|
||||
converter.py — matrix-nio Event → IncomingEvent, OutgoingEvent → Matrix API
|
||||
bot.py — точка входа, клиент
|
||||
_template.py — шаблон для новой поверхности
|
||||
|
||||
platform/
|
||||
interface.py — Protocol: PlatformClient
|
||||
mock.py — MockPlatformClient
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Входящие события (Incoming)
|
||||
|
||||
Всё что пользователь делает конвертируется в одно из трёх:
|
||||
|
||||
### IncomingMessage
|
||||
Обычное сообщение — текст, файл, голос.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class IncomingMessage:
|
||||
user_id: str # "tg_123456" | "@user:matrix.org"
|
||||
platform: str # "telegram" | "matrix"
|
||||
chat_id: str # "C1" | "C2" — ID чата в воркспейсе платформы
|
||||
text: str
|
||||
attachments: list[Attachment]
|
||||
reply_to: str | None # message_id если это ответ на сообщение
|
||||
```
|
||||
|
||||
### IncomingCommand
|
||||
Команда управления — `/start`, `!new`, `/settings`.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class IncomingCommand:
|
||||
user_id: str
|
||||
platform: str
|
||||
chat_id: str
|
||||
command: str # "start" | "new" | "settings" | "archive" ...
|
||||
args: list[str] # аргументы после команды
|
||||
```
|
||||
|
||||
### IncomingCallback
|
||||
Нажатие кнопки или реакции — подтверждение, переключение скилла.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class IncomingCallback:
|
||||
user_id: str
|
||||
platform: str
|
||||
chat_id: str
|
||||
action: str # "confirm" | "cancel" | "toggle_skill" | "connect" ...
|
||||
payload: dict # {"skill": "browser"} | {"action_id": "abc123"} ...
|
||||
```
|
||||
|
||||
### Attachment
|
||||
Вложение, нормализованное из любого источника.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Attachment:
|
||||
type: str # "image" | "document" | "audio" | "video"
|
||||
url: str | None # ссылка если доступна
|
||||
content: bytes | None # содержимое если скачано
|
||||
filename: str | None
|
||||
mime_type: str | None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Исходящие события (Outgoing)
|
||||
|
||||
Всё что ядро хочет показать пользователю:
|
||||
|
||||
### OutgoingMessage
|
||||
Ответ агента или системное сообщение.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OutgoingMessage:
|
||||
chat_id: str
|
||||
text: str
|
||||
parse_mode: str # "markdown" | "plain"
|
||||
attachments: list[Attachment]
|
||||
reply_to: str | None
|
||||
```
|
||||
|
||||
### OutgoingUI
|
||||
Интерактивные элементы — кнопки, меню, переключатели.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OutgoingUI:
|
||||
chat_id: str
|
||||
text: str
|
||||
buttons: list[UIButton]
|
||||
|
||||
@dataclass
|
||||
class UIButton:
|
||||
label: str
|
||||
action: str # action для IncomingCallback
|
||||
payload: dict
|
||||
style: str # "primary" | "danger" | "secondary"
|
||||
```
|
||||
|
||||
Telegram рендерит это как InlineKeyboard.
|
||||
Matrix рендерит как текст с описанием реакций или HTML-кнопки.
|
||||
|
||||
### OutgoingNotification
|
||||
Асинхронное уведомление — агент закончил долгую задачу.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OutgoingNotification:
|
||||
chat_id: str
|
||||
text: str
|
||||
level: str # "info" | "warning" | "success" | "error"
|
||||
```
|
||||
|
||||
### OutgoingTyping
|
||||
Индикатор что агент думает.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OutgoingTyping:
|
||||
chat_id: str
|
||||
is_typing: bool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Жизненный цикл (Lifecycle)
|
||||
|
||||
Унифицированные события для управления чатами и подключением.
|
||||
|
||||
### ChatContext
|
||||
Состояние чата — общее для всех поверхностей.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ChatContext:
|
||||
chat_id: str # "C1" | "C2" — ID в воркспейсе платформы
|
||||
display_name: str # «Чат 1» | «Анализ рынка»
|
||||
platform: str
|
||||
surface_ref: str # room_id в Matrix | topic_id в Telegram
|
||||
session_id: str | None # активная сессия платформы
|
||||
created_at: datetime
|
||||
is_archived: bool
|
||||
```
|
||||
|
||||
### AuthFlow
|
||||
Флоу аутентификации — одинаков для всех поверхностей.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class AuthFlow:
|
||||
user_id: str
|
||||
platform: str
|
||||
state: str # "pending" | "code_sent" | "confirmed" | "failed"
|
||||
platform_user_id: str | None
|
||||
```
|
||||
|
||||
### ConfirmationRequest
|
||||
Запрос подтверждения опасного действия — от агента к пользователю.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ConfirmationRequest:
|
||||
action_id: str
|
||||
chat_id: str
|
||||
description: str # «Отправить письмо на vasya@mail.ru»
|
||||
risk_level: str # "low" | "medium" | "high"
|
||||
expires_at: datetime
|
||||
```
|
||||
|
||||
Telegram показывает как Inline-кнопки.
|
||||
Matrix показывает как реакции 👍 / ❌.
|
||||
Ядро не знает как именно — только получает `IncomingCallback` с `action: "confirm"`.
|
||||
|
||||
---
|
||||
|
||||
## SettingsAction
|
||||
|
||||
Унифицированные действия в настройках — одинаковы для Telegram и Matrix.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SettingsAction:
|
||||
action: str # "connect" | "disconnect" | "toggle_skill" | ...
|
||||
payload: dict
|
||||
|
||||
# Примеры:
|
||||
SettingsAction(action="connect", payload={"service": "gmail"})
|
||||
SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True})
|
||||
SettingsAction(action="set_soul", payload={"field": "name", "value": "Лямбда"})
|
||||
SettingsAction(action="set_safety", payload={"trigger": "email-send", "enabled": True})
|
||||
```
|
||||
|
||||
Ядро обрабатывает `SettingsAction` одинаково. Откуда пришло — не важно.
|
||||
|
||||
---
|
||||
|
||||
## PaymentEvent
|
||||
|
||||
Заглушка для биллинга — реализует другая команда.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PaymentRequired:
|
||||
user_id: str
|
||||
reason: str # "limit_reached" | "feature_locked"
|
||||
current_plan: str
|
||||
```
|
||||
|
||||
Поверхность получила `PaymentRequired` → показала заглушку.
|
||||
Содержимое заглушки потом наполняет команда биллинга.
|
||||
|
||||
---
|
||||
|
||||
## Как написать новую поверхность
|
||||
|
||||
Скопируй `adapter/_template.py` и реализуй три метода:
|
||||
|
||||
```python
|
||||
class MySurfaceAdapter:
|
||||
|
||||
def parse_incoming(self, raw_event) -> IncomingMessage | IncomingCommand | IncomingCallback:
|
||||
"""Конвертировать нативное событие в протокольную структуру."""
|
||||
...
|
||||
|
||||
def render_outgoing(self, event: OutgoingMessage | OutgoingUI | OutgoingNotification) -> any:
|
||||
"""Конвертировать протокольную структуру в нативный формат."""
|
||||
...
|
||||
|
||||
async def send(self, rendered) -> None:
|
||||
"""Отправить нативным способом."""
|
||||
...
|
||||
```
|
||||
|
||||
Дальше подключи адаптер в точку входа и передай `core.handler.handle` как callback.
|
||||
Всё остальное уже работает.
|
||||
|
||||
---
|
||||
|
||||
## Что ядро даёт бесплатно
|
||||
|
||||
Любая новая поверхность получает без дополнительного кода:
|
||||
|
||||
- управление сессиями (создание, переключение, закрытие)
|
||||
- управление чатами (`ChatContext`)
|
||||
- аутентификацию (`AuthFlow`)
|
||||
- подтверждение действий (`ConfirmationRequest`)
|
||||
- все настройки (коннекторы, скиллы, SOUL, безопасность, подписка)
|
||||
- интеграцию с платформой через `PlatformClient`
|
||||
- обработку ошибок платформы
|
||||
|
||||
---
|
||||
|
||||
## Замена MockPlatformClient на реальный SDK
|
||||
|
||||
Вся работа с платформой идёт через `PlatformClient` протокол:
|
||||
|
||||
```python
|
||||
class PlatformClient(Protocol):
|
||||
async def get_or_create_user(self, user_id: str, platform: str) -> User: ...
|
||||
async def create_session(self, user_id: str, chat_id: str) -> Session: ...
|
||||
async def send_message(self, session_id: str, text: str, attachments: list) -> AgentResponse: ...
|
||||
async def close_session(self, session_id: str) -> None: ...
|
||||
async def get_chat_history(self, user_id: str, chat_id: str) -> list[Message]: ...
|
||||
async def get_settings(self, user_id: str) -> UserSettings: ...
|
||||
async def update_settings(self, user_id: str, action: SettingsAction) -> None: ...
|
||||
```
|
||||
|
||||
`MockPlatformClient` реализует этот протокол сейчас.
|
||||
Реальный SDK — тоже реализует этот протокол, заменяя один файл.
|
||||
Адаптеры поверхностей и ядро не меняются вообще.
|
||||
Loading…
Add table
Add a link
Reference in a new issue