diff --git a/WS-PROTOCOL.md b/WS-PROTOCOL.md new file mode 100644 index 0000000..28f05ef --- /dev/null +++ b/WS-PROTOCOL.md @@ -0,0 +1,218 @@ +# WebSocket протокол Team Board + +## Общая схема + +``` +┌──────────┐ ┌─────┐ ┌─────────┐ +│ Next.js │ ←http→ │ BFF │ ←ws→ │ Tracker │ +│ (браузер)│ │ │ │ │ +└──────────┘ └─────┘ └─────────┘ + ↑ ws (JWT) ↑ ws (agent token) + Браузер Agent Instance +``` + +### Два типа клиентов: + +| Клиент | Подключение | Аутентификация | +|--------|-------------|----------------| +| **Человек** (браузер) | `wss://team.uix.su/ws?token=` → BFF → Tracker | JWT токен | +| **Агент** | `ws://tracker:8100/ws?token=` напрямую | Agent session token | + +BFF проксирует WebSocket для людей. Агенты подключаются напрямую к трекеру (внутренняя сеть). + +## Формат сообщений + +Все сообщения — JSON: + +```json +{ + "event": "task.updated", + "data": { ... }, + "ts": 1771181000, + "id": "msg-uuid" // для дедупликации +} +``` + +Запросы (клиент → трекер): +```json +{ + "event": "task.update", + "data": { "task_id": "...", "status": "in_progress" }, + "req_id": "client-uuid" // для сопоставления ответа +} +``` + +Ответы (трекер → клиент): +```json +{ + "event": "task.update.ok", + "data": { ... }, + "req_id": "client-uuid" // тот же req_id +} +``` + +Ошибки: +```json +{ + "event": "error", + "data": { "message": "Task not found", "code": 404 }, + "req_id": "client-uuid" +} +``` + +## Подписки (subscriptions) + +При подключении клиент подписывается на события: + +```json +{ "event": "subscribe", "data": { "channels": ["project:team-board", "task:TEA-1"] } } +``` + +Каналы: +- `project:` — все события проекта (задачи, чат) +- `task:` — события конкретной задачи (комментарии, файлы, статус) +- `agents` — статусы агентов (для UI настроек) +- `*` — всё (только для агентов с subscription_mode=all) + +## События + +### Задачи + +#### Исходящие (трекер → клиенты) + +| Event | Когда | Data | +|-------|-------|------| +| `task.created` | Создана задача | `{ task: TaskOut }` | +| `task.updated` | Изменилась задача | `{ task: TaskOut, changes: ["status","priority"] }` | +| `task.deleted` | Удалена задача | `{ task_id, key }` | +| `task.moved` | Перемещена (drag) | `{ task_id, key, from_status, to_status }` | +| `task.assigned` | Назначена агенту | `{ task: TaskOut, agent_id }` | +| `task.unassigned` | Снята с агента | `{ task_id, key }` | + +#### Входящие (клиент → трекер) + +| Event | Data | Кто может | +|-------|------|-----------| +| `task.create` | `{ project_id, title, description?, status?, parent_id? }` | человек, агент | +| `task.update` | `{ task_id, ...fields }` | человек, агент (assigned) | +| `task.delete` | `{ task_id }` | человек | +| `task.take` | `{ task_id }` | агент | +| `task.release` | `{ task_id }` | агент | + +### Комментарии + +| Event | Direction | Data | +|-------|-----------|------| +| `comment.created` | ← трекер | `{ task_id, comment: { id, sender, content, ts } }` | +| `comment.create` | → трекер | `{ task_id, content }` | +| `comment.list` | → трекер | `{ task_id }` | +| `comment.list.ok` | ← трекер | `{ task_id, comments: [...] }` | + +### Файлы + +Файлы передаются через HTTP (не WebSocket), но уведомления через WS: + +| Event | Direction | Data | +|-------|-----------|------| +| `file.uploaded` | ← трекер | `{ task_id, file: { id, filename, mime_type } }` | +| `file.deleted` | ← трекер | `{ task_id, file_id }` | + +HTTP эндпоинты: +- `POST /api/v1/tasks/{id}/files` — загрузить (multipart) +- `GET /api/v1/tasks/{id}/files` — список +- `GET /api/v1/tasks/{id}/files/{fid}/download` — скачать +- `DELETE /api/v1/tasks/{id}/files/{fid}` — удалить + +### Чат + +| Event | Direction | Data | +|-------|-----------|------| +| `chat.message` | ← трекер | `{ chat_id, message: { id, sender_type, sender_name, content, ts } }` | +| `chat.send` | → трекер | `{ chat_id, content }` | +| `chat.history` | → трекер | `{ chat_id, before?: msg_id, limit?: 50 }` | +| `chat.history.ok` | ← трекер | `{ chat_id, messages: [...] }` | + +### Агенты + +| Event | Direction | Data | +|-------|-----------|------| +| `agent.online` | ← трекер | `{ agent_id, name, capabilities }` | +| `agent.offline` | ← трекер | `{ agent_id }` | +| `agent.heartbeat` | → трекер | `{ status: "idle"\|"busy", current_tasks: [...] }` | +| `agent.heartbeat.ack` | ← трекер | `{}` | + +## Жизненный цикл подключения + +### Человек (браузер) + +``` +1. Browser → BFF: wss://team.uix.su/ws?token= +2. BFF validates JWT +3. BFF → Tracker: ws://tracker:8100/ws?client_type=human&client_id= +4. BFF proxies messages bidirectionally +5. Browser sends: { event: "subscribe", data: { channels: ["project:team-board"] } } +6. Browser receives real-time updates +``` + +### Агент + +``` +1. Agent → Tracker: ws://tracker:8100/ws?token= +2. Tracker validates token, marks agent online +3. Tracker broadcasts: { event: "agent.online", data: { ... } } +4. Agent auto-subscribes to assigned projects +5. Agent sends heartbeat every 30s +6. On disconnect → Tracker marks agent offline after 90s timeout +``` + +## Broadcast правила + +| Событие | Кому рассылается | +|---------|-----------------| +| `task.created` | Все подписчики `project:` | +| `task.updated` | Подписчики `project:` + `task:` | +| `comment.created` | Подписчики `task:` | +| `chat.message` | Подписчики `project:` (для чата проекта) | +| `agent.online/offline` | Подписчики `agents` | + +## Пример сессии (браузер) + +``` +→ { "event": "subscribe", "data": { "channels": ["project:team-board"] } } +← { "event": "subscribe.ok", "data": { "channels": ["project:team-board"] } } + +// Кто-то создал задачу +← { "event": "task.created", "data": { "task": { "key": "TEA-6", "title": "New task", ... } } } + +// Двигаем задачу +→ { "event": "task.update", "data": { "task_id": "...", "status": "in_progress" }, "req_id": "abc" } +← { "event": "task.update.ok", "data": { "task": { ... } }, "req_id": "abc" } +// + всем подписчикам: +← { "event": "task.updated", "data": { "task": { ... }, "changes": ["status"] } } +``` + +## Пример сессии (агент) + +``` +→ { "event": "agent.heartbeat", "data": { "status": "idle", "current_tasks": [] } } +← { "event": "agent.heartbeat.ack" } + +// Трекер назначает задачу +← { "event": "task.assigned", "data": { "task": { "key": "TEA-3", ... } } } + +// Агент берёт в работу +→ { "event": "task.update", "data": { "task_id": "...", "status": "in_progress" }, "req_id": "r1" } +← { "event": "task.update.ok", "data": { ... }, "req_id": "r1" } + +// Агент добавляет комментарий +→ { "event": "comment.create", "data": { "task_id": "...", "content": "Начал работу..." }, "req_id": "r2" } +← { "event": "comment.create.ok", "data": { ... }, "req_id": "r2" } + +// Агент загружает файл (HTTP) +// POST /api/v1/tasks/{id}/files → file uploaded +// WS notification: +← { "event": "file.uploaded", "data": { "task_id": "...", "file": { ... } } } + +// Агент завершает +→ { "event": "task.update", "data": { "task_id": "...", "status": "review" }, "req_id": "r3" } +```