Compare commits
No commits in common. "main" and "master" have entirely different histories.
@ -1,89 +0,0 @@
|
|||||||
# AGENT-PLAN.md — План работ: Агенты + Трекер
|
|
||||||
|
|
||||||
## Фаза 1: Конфиг агента (agent.json → минимум)
|
|
||||||
|
|
||||||
### 1.1 Разделение конфигов ✅
|
|
||||||
- [x] **agent.json** — только подключение: `token`, `tracker_url`, `ws_url`, `transport`, `session_id`
|
|
||||||
- [x] **config.json** — LLM: `api_key`, `model`, `provider`
|
|
||||||
- [x] Picogent `loadAgentConfig()` — читает оба файла, мержит (env > config.json > agent.json)
|
|
||||||
- [x] name, slug, capabilities теперь приходят из Tracker (auth.ok)
|
|
||||||
|
|
||||||
### 1.2 Трекер: расширить auth_ok ✅
|
|
||||||
- [x] WS `auth_ok` возвращает `agent_config`: model, provider, prompt, chat_listen, task_listen, max_concurrent_tasks, capabilities
|
|
||||||
- [x] AgentConfig model: добавлены `provider`, `max_concurrent_tasks`
|
|
||||||
- [x] Picogent мержит remote config с локальными override-ами
|
|
||||||
|
|
||||||
### 1.3 Горячее обновление конфига ✅
|
|
||||||
- [x] Новый WS event `config.updated` (WSEventType.CONFIG_UPDATED)
|
|
||||||
- [x] Tracker отправляет при PATCH /members/{id} с agent_config
|
|
||||||
- [x] Picogent обрабатывает event, обновляет runtime model/provider/prompt
|
|
||||||
|
|
||||||
### 1.4 Systemd template для мульти-агент ✅
|
|
||||||
- [x] `picogent@.service`: `ExecStart=node dist/index.js /root/projects/team-board/agents/%i/`
|
|
||||||
- [x] `systemctl start picogent@coder`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 2: Память агента ✅ РЕАЛИЗОВАНО
|
|
||||||
|
|
||||||
### Структура (двухуровневая, per-project)
|
|
||||||
```
|
|
||||||
agents/{slug}/
|
|
||||||
AGENT.md # инструкции (статичные)
|
|
||||||
memory/
|
|
||||||
agent.md # личные уроки, стиль (грузится ВСЕГДА)
|
|
||||||
projects/
|
|
||||||
{project_uuid}/
|
|
||||||
context.md # архитектура, решения (грузится per-task)
|
|
||||||
recent.md # последние действия (rolling window, ~20 записей)
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Почему UUID, а не slug?** Slug проекта может измениться (переименование).
|
|
||||||
> UUID — immutable. Память агента не ломается при переименовании проекта.
|
|
||||||
|
|
||||||
### Загрузка контекста
|
|
||||||
1. Task приходит с `project_id` (UUID)
|
|
||||||
2. Bootstrap: `AGENT.md` + `memory/agent.md` + `memory/projects/{project_uuid}/context.md` + `recent.md`
|
|
||||||
3. Итого ~7K chars вместо 15K (экономия ~50%)
|
|
||||||
4. Кросс-проектная инфа — on-demand через MCP-тулзы, НЕ в bootstrap
|
|
||||||
|
|
||||||
### Компакция
|
|
||||||
- После завершения задачи: LLM суммаризирует сессию → 3-5 строк → append `recent.md`
|
|
||||||
- `recent.md`: rolling window, последние ~20 записей, старые вытесняются
|
|
||||||
- Weekly: LLM ревьюит `recent.md` → обновляет `context.md` (long-term)
|
|
||||||
- Сырые JSONL сессии: удалять через 7 дней (суммаризация уже произошла)
|
|
||||||
|
|
||||||
### Кросс-проектный доступ
|
|
||||||
- Агент = **member** в своём проекте, **viewer** в связанных
|
|
||||||
- Viewer = read-only (list_tasks, list_project_files через REST)
|
|
||||||
- Viewer НЕ получает WS push-события (экономия токенов)
|
|
||||||
- Три уровня контроля: AGENT.md → MCP-тулзы → API (403)
|
|
||||||
|
|
||||||
### Shared knowledge
|
|
||||||
- Project docs (README, ARCHITECTURE) = shared, живут в трекере/репо
|
|
||||||
- Personal memory = agent-only, не шарится между агентами
|
|
||||||
- Координация — через project docs или чат, не через чужую память
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 3: UUID-based workspace (отложено)
|
|
||||||
|
|
||||||
- [ ] Workspace именуется по `member.id` (UUID) вместо slug
|
|
||||||
- [ ] Симлинки `agents/coder → agents/{uuid}` для удобства
|
|
||||||
- [ ] Переименование slug не ломает workspace
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Решения (принятые)
|
|
||||||
|
|
||||||
| Решение | Дата |
|
|
||||||
|---------|------|
|
|
||||||
| agent.json = token + tracker_url, config.json = LLM config | 2026-02-25 |
|
|
||||||
| Трекер = source of truth для routing/orchestration | 2026-02-25 |
|
|
||||||
| Агент не раскрывает api_key трекеру | 2026-02-25 |
|
|
||||||
| Directory mode для мульти-агент (уже работает) | 2026-02-25 |
|
|
||||||
| Per-project память: agent.md (always) + projects/{uuid}/ (per-task) | 2026-02-25 |
|
|
||||||
| Папки памяти по UUID проекта (не slug — slug может измениться) | 2026-02-25 |
|
|
||||||
| Viewer-роль: read-only доступ к чужим проектам | 2026-02-25 |
|
|
||||||
| Viewer без WS push — только REST pull | 2026-02-25 |
|
|
||||||
| Shared knowledge через project docs, не через чужую память | 2026-02-25 |
|
|
||||||
@ -1,301 +0,0 @@
|
|||||||
# Architecture Review — Team Board
|
|
||||||
Дата: 2026-02-23
|
|
||||||
|
|
||||||
## 1. Соответствие PRD
|
|
||||||
|
|
||||||
### 4.1 Управление проектами ✅ ХОРОШО
|
|
||||||
- **Архитектура:** Полностью покрывает, модель Project с multi-repo поддержкой
|
|
||||||
- **Код:** Реализовано 100%, все API endpoints работают, UI компоненты созданы
|
|
||||||
- **Расхождения:** Нет
|
|
||||||
|
|
||||||
### 4.2 Управление задачами (Kanban) ⚠️ ЧАСТИЧНО
|
|
||||||
- **Архитектура:** Покрывает полностью, модель Task с подзадачами, зависимостями, watchers
|
|
||||||
- **Код:** Базовые CRUD есть, но отсутствуют специальные операции
|
|
||||||
- **Расхождения:**
|
|
||||||
- ❌ `POST /api/v1/tasks/{id}/take` — не реализован (БФФ проксирует, но в трекере нет)
|
|
||||||
- ❌ `POST /api/v1/tasks/{id}/reject` — не реализован
|
|
||||||
- ❌ `POST /api/v1/tasks/{id}/assign` — не реализован
|
|
||||||
- ❌ `POST /api/v1/tasks/{id}/watch` — не реализован
|
|
||||||
- ✅ Базовые CRUD работают
|
|
||||||
|
|
||||||
### 4.3 Чат и коммуникация ✅ ХОРОШО
|
|
||||||
- **Архитектура:** Unified Message модель реализована корректно
|
|
||||||
- **Код:** Message с chat_id/task_id работает, WS chat.send функционирует
|
|
||||||
- **Расхождения:**
|
|
||||||
- ⚠️ Голосовые сообщения (voice_url) — поле есть в модели, но API загрузки нет
|
|
||||||
|
|
||||||
### 4.4 AI агенты ⚠️ ПРОБЛЕМЫ
|
|
||||||
- **Архитектура:** Модель Member unified, AgentConfig детализирован
|
|
||||||
- **Код:** Модели реализованы, но проблемы с функциональностью
|
|
||||||
- **Расхождения:**
|
|
||||||
- ❌ Agent management UI — полностью отсутствует
|
|
||||||
- ❌ MCP Tools — не реализованы
|
|
||||||
- ❌ Фильтрация WS событий по capabilities — упрощена: humans получают ВСЁ
|
|
||||||
- ❌ REST API token auth для агентов — не работает (только WS auth)
|
|
||||||
|
|
||||||
### 4.5 Файлы и вложения ❌ НЕ РЕАЛИЗОВАНО
|
|
||||||
- **Архитектура:** Модель Attachment есть
|
|
||||||
- **Код:** Полностью отсутствует
|
|
||||||
- **Расхождения:**
|
|
||||||
- ❌ `POST /api/v1/messages/{id}/attachments` — не реализован
|
|
||||||
- ❌ `GET /api/v1/attachments/{id}` — не реализован
|
|
||||||
- ❌ Хранение файлов — не реализовано
|
|
||||||
|
|
||||||
### 4.6 Аутентификация и авторизация ⚠️ ЧАСТИЧНО
|
|
||||||
- **Архитектура:** JWT + Token auth описаны корректно
|
|
||||||
- **Код:** JWT через BFF работает, agent WS auth работает
|
|
||||||
- **Расхождения:**
|
|
||||||
- ❌ Token auth для REST API агентов — проверка не реализована
|
|
||||||
- ✅ BFF JWT auth работает
|
|
||||||
- ✅ WS token auth для агентов работает
|
|
||||||
|
|
||||||
### 4.7 WebSocket и реал-тайм ⚠️ ПРОБЛЕМЫ
|
|
||||||
- **Архитектура:** Подробно описан протокол, фильтрация событий
|
|
||||||
- **Код:** Базовые WS работают, но упрощённая архитектура
|
|
||||||
- **Расхождения:**
|
|
||||||
- ❌ Task события (task.created/updated/assigned) — не транслируются
|
|
||||||
- ⚠️ Упрощённая фильтрация: humans/bridges получают ВСЁ, agents — по подпискам
|
|
||||||
- ✅ auth, heartbeat, chat.send, project.subscribe работают
|
|
||||||
|
|
||||||
### 4.8 Steps (этапы задач) ✅ ХОРОШО
|
|
||||||
- **Архитектура:** Корректно описаны как чеклист внутри задачи
|
|
||||||
- **Код:** Полная реализация: модель, API, UI в TaskModal
|
|
||||||
- **Расхождения:** Нет
|
|
||||||
|
|
||||||
## 2. Архитектурные проблемы
|
|
||||||
|
|
||||||
### 2.1 WebSocket: Один слот на slug — КРИТИЧЕСКАЯ ПРОБЛЕМА
|
|
||||||
**Проблема:** `ConnectionManager.clients: dict[str, ConnectedClient]` — один слот на slug.
|
|
||||||
```python
|
|
||||||
# manager.py:15
|
|
||||||
self.clients: dict[str, ConnectedClient] = {} # slug → client
|
|
||||||
```
|
|
||||||
**Последствия:**
|
|
||||||
- Множественные вкладки одного пользователя вытесняют друг друга
|
|
||||||
- Агент при переподключении теряет предыдущую сессию
|
|
||||||
- Race conditions при одновременных подключениях
|
|
||||||
|
|
||||||
**Решение:** `dict[str, list[ConnectedClient]]` или уникальные session_id.
|
|
||||||
|
|
||||||
### 2.2 Task Events не транслируются
|
|
||||||
**Проблема:** В коде есть `broadcast_task_event`, но никто её не вызывает.
|
|
||||||
```python
|
|
||||||
# manager.py:47
|
|
||||||
async def broadcast_task_event(self, project_id: str, event_type: str, data: dict):
|
|
||||||
```
|
|
||||||
**Последствия:** Агенты не получают уведомления о создании/обновлении задач.
|
|
||||||
|
|
||||||
**Решение:** Интегрировать в REST API endpoints для задач.
|
|
||||||
|
|
||||||
### 2.3 REST API авторизация агентов не работает
|
|
||||||
**Проблема:** Токен проверяется только в WebSocket, REST API для агентов полностью открыт.
|
|
||||||
```python
|
|
||||||
# BFF проксирует агентские операции, но агенты не могут ходить напрямую к Tracker
|
|
||||||
```
|
|
||||||
**Последствия:** Агенты не могут делать REST вызовы без BFF.
|
|
||||||
|
|
||||||
**Решение:** Добавить token auth middleware в Tracker REST API.
|
|
||||||
|
|
||||||
### 2.4 BFF on_behalf_of создаёт collision risk
|
|
||||||
**Проблема:** Bridge может зарегистрировать любой `on_behalf_of` slug, даже несуществующий.
|
|
||||||
```python
|
|
||||||
# handler.py:88-95
|
|
||||||
if on_behalf_of and member.type == "bridge":
|
|
||||||
# Если пользователь не найден, используется синтетический slug
|
|
||||||
effective_slug = on_behalf_of
|
|
||||||
```
|
|
||||||
**Последствия:** Коллизии slug при подключении реального пользователя.
|
|
||||||
|
|
||||||
**Решение:** Строгая валидация on_behalf_of или префиксы для synthetic slugs.
|
|
||||||
|
|
||||||
### 2.5 Упрощённая фильтрация событий
|
|
||||||
**Проблема:** Humans/bridges получают ВСЕ события без фильтрации.
|
|
||||||
```python
|
|
||||||
# manager.py:36
|
|
||||||
if client.member_type in ("human", "bridge"):
|
|
||||||
await self.send_to(slug, {"type": "message.new", "data": message})
|
|
||||||
continue
|
|
||||||
```
|
|
||||||
**Последствия:** Лишний трафик, невозможность настроить подписки для людей.
|
|
||||||
|
|
||||||
**Решение:** Единая логика фильтрации для всех типов участников.
|
|
||||||
|
|
||||||
## 3. Расхождения документация ↔ код
|
|
||||||
|
|
||||||
### 3.1 TRACKER-PROTOCOL.md vs реальный протокол
|
|
||||||
**Документ описывает:**
|
|
||||||
- task.created/updated/assigned события
|
|
||||||
- Детальную фильтрацию по listen_mode и capabilities
|
|
||||||
- REST API endpoints для task operations
|
|
||||||
|
|
||||||
**Реальный код:**
|
|
||||||
- Task события не отправляются
|
|
||||||
- Упрощённая фильтрация (humans получают всё)
|
|
||||||
- Task API endpoints отсутствуют
|
|
||||||
|
|
||||||
### 3.2 ARCHITECTURE.md vs WS manager
|
|
||||||
**Документ описывает:**
|
|
||||||
- Фильтрацию по capabilities и listen modes
|
|
||||||
- Отдельные подписки для humans
|
|
||||||
|
|
||||||
**Реальный код:**
|
|
||||||
- Capabilities не используются в фильтрации
|
|
||||||
- Humans получают всё без подписок
|
|
||||||
|
|
||||||
### 3.3 CONCEPTS.md vs Agent REST auth
|
|
||||||
**Документ описывает:**
|
|
||||||
- REST API с token авторизацией для агентов
|
|
||||||
- MCP Tools полный набор
|
|
||||||
|
|
||||||
**Реальный код:**
|
|
||||||
- REST API открыт для всех
|
|
||||||
- MCP Tools отсутствуют
|
|
||||||
|
|
||||||
### 3.4 PRD vs отсутствующие features
|
|
||||||
**PRD объявляет реализованными:**
|
|
||||||
- Agent management UI
|
|
||||||
- Attachments API
|
|
||||||
- Task special operations
|
|
||||||
|
|
||||||
**Реальный код:**
|
|
||||||
- Эти features полностью отсутствуют
|
|
||||||
|
|
||||||
## 4. Рекомендации
|
|
||||||
|
|
||||||
### P0: Блокеры
|
|
||||||
|
|
||||||
#### P0.1 Исправить WebSocket collision
|
|
||||||
```python
|
|
||||||
# manager.py
|
|
||||||
class ConnectionManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.clients: dict[str, list[ConnectedClient]] = {} # slug → clients[]
|
|
||||||
self.sessions: dict[str, ConnectedClient] = {} # session_id → client
|
|
||||||
```
|
|
||||||
**Без этого:** Система не работает корректно при multiple connections.
|
|
||||||
|
|
||||||
#### P0.2 Добавить REST token auth для агентов
|
|
||||||
```python
|
|
||||||
# tracker/api/middleware.py
|
|
||||||
async def verify_agent_token(request: Request):
|
|
||||||
auth = request.headers.get("Authorization")
|
|
||||||
if auth and auth.startswith("Bearer "):
|
|
||||||
token = auth[7:]
|
|
||||||
# Проверить в БД Member.token
|
|
||||||
```
|
|
||||||
**Без этого:** Агенты не могут использовать API напрямую.
|
|
||||||
|
|
||||||
#### P0.3 Реализовать task events
|
|
||||||
```python
|
|
||||||
# tracker/api/tasks.py после create/update/delete
|
|
||||||
await broadcast_task_event(task.project_id, "task.created", task_data)
|
|
||||||
```
|
|
||||||
**Без этого:** Агенты не видят изменения задач.
|
|
||||||
|
|
||||||
### P1: Важно
|
|
||||||
|
|
||||||
#### P1.1 Реализовать task special operations
|
|
||||||
- `POST /api/v1/tasks/{id}/take` — атомарное взятие задачи
|
|
||||||
- `POST /api/v1/tasks/{id}/reject` — отклонение с причиной
|
|
||||||
- `POST /api/v1/tasks/{id}/assign` — назначение
|
|
||||||
- `POST /api/v1/tasks/{id}/watch` — подписка на задачу
|
|
||||||
|
|
||||||
**Почему:** Это ключевые операции для работы агентов с задачами.
|
|
||||||
|
|
||||||
#### P1.2 Создать Agent Management UI
|
|
||||||
- Страница `/agents` с списком агентов
|
|
||||||
- Создание агентов с генерацией токенов
|
|
||||||
- Просмотр статусов и capabilities
|
|
||||||
|
|
||||||
**Почему:** Без UI управление агентами невозможно.
|
|
||||||
|
|
||||||
#### P1.3 Унифицировать фильтрацию событий
|
|
||||||
```python
|
|
||||||
async def should_receive_event(client: ConnectedClient, event_type: str, data: dict) -> bool:
|
|
||||||
# Единая логика для всех типов участников
|
|
||||||
# Проверка подписок, listen_mode, capabilities
|
|
||||||
```
|
|
||||||
**Почему:** Текущая архитектура не масштабируется.
|
|
||||||
|
|
||||||
#### P1.4 Исправить on_behalf_of логику
|
|
||||||
- Валидировать существование пользователя
|
|
||||||
- Использовать префиксы для synthetic slugs: `web-{slug}`
|
|
||||||
- Логировать все proxy подключения
|
|
||||||
|
|
||||||
**Почему:** Предотвращает коллизии и security issues.
|
|
||||||
|
|
||||||
### P2: Хорошо бы
|
|
||||||
|
|
||||||
#### P2.1 Реализовать Attachments API
|
|
||||||
- `POST /api/v1/messages/{id}/attachments` — загрузка
|
|
||||||
- `GET /api/attachments/{id}` — скачивание
|
|
||||||
- Хранение на диске по storage_path
|
|
||||||
|
|
||||||
**Почему:** Команды часто работают с файлами.
|
|
||||||
|
|
||||||
#### P2.2 Добавить MCP Tools для агентов
|
|
||||||
- create_task, take_task, update_task
|
|
||||||
- send_message, list_messages
|
|
||||||
- watch_task, unwatch_task
|
|
||||||
|
|
||||||
**Почему:** Агентам нужны инструменты для работы.
|
|
||||||
|
|
||||||
#### P2.3 Улучшить monitoring
|
|
||||||
- Health check endpoints `/health`
|
|
||||||
- Метрики WebSocket connections
|
|
||||||
- Логирование всех WS событий с timestamps
|
|
||||||
|
|
||||||
**Почему:** Проще дебажить и мониторить продакшен.
|
|
||||||
|
|
||||||
## 5. Обновлённая архитектурная диаграмма
|
|
||||||
|
|
||||||
### Текущая архитектура (как есть)
|
|
||||||
```
|
|
||||||
Web Client ——HTTP/JWT——→ BFF ——HTTP/Token——→ Tracker
|
|
||||||
↓ ↑ ↑
|
|
||||||
WS/JWT————————————————┘ │
|
|
||||||
│ WS/Token
|
|
||||||
Picogent Agent ———————————————————————————————————┘
|
|
||||||
(REST auth не работает)
|
|
||||||
|
|
||||||
ConnectionManager: dict[slug → client] ← ПРОБЛЕМА
|
|
||||||
Фильтрация: humans=ALL, agents=subscription ← УПРОЩЕНО
|
|
||||||
Task events: НЕ ОТПРАВЛЯЮТСЯ ← ПРОБЛЕМА
|
|
||||||
```
|
|
||||||
|
|
||||||
### Целевая архитектура (как должно быть)
|
|
||||||
```
|
|
||||||
Web Client ——HTTP/JWT——→ BFF ——HTTP/Token——→ Tracker
|
|
||||||
↓ ↑ ↑ ↑
|
|
||||||
WS/JWT————————————————┘ │ │ WS/Token
|
|
||||||
│ │
|
|
||||||
Picogent Agent ————HTTP/Token—————————————————┘ │
|
|
||||||
├————————WS/Token————————————————————────┘
|
|
||||||
└————————MCP Tools———————┘
|
|
||||||
|
|
||||||
ConnectionManager:
|
|
||||||
dict[slug → clients[]] ← ФИКС
|
|
||||||
dict[session_id → client] ← ФИКС
|
|
||||||
|
|
||||||
Фильтрация: unified для всех типов ← УЛУЧШЕНИЕ
|
|
||||||
Task events: все операции транслируются ← ФИКС
|
|
||||||
Agent REST: token auth работает ← ФИКС
|
|
||||||
```
|
|
||||||
|
|
||||||
### Компоненты как должны быть
|
|
||||||
| Компонент | Порт | Протоколы | Изменения |
|
|
||||||
|-----------|------|-----------|-----------|
|
|
||||||
| **Tracker** | 8100 | HTTP+WS (token auth) | + Task events, + Agent REST auth |
|
|
||||||
| **BFF** | 8200 | HTTP+WS (JWT auth) | + on_behalf_of validation |
|
|
||||||
| **Web Client** | 3100 | HTTP+WS через BFF | + Agent Management UI |
|
|
||||||
| **Picogent** | — | HTTP+WS к Tracker | + MCP Tools |
|
|
||||||
|
|
||||||
### Протоколы взаимодействия
|
|
||||||
- **Web ↔ BFF:** JWT auth, WS proxy с session management
|
|
||||||
- **BFF ↔ Tracker:** Token auth, on_behalf_of для web users
|
|
||||||
- **Agent ↔ Tracker:** Token auth для HTTP и WS, unified
|
|
||||||
- **Events:** Task+Message события через WS с единой фильтрацией
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Итог:** Архитектура в целом правильная, но много критических недоработок в реализации. Основная проблема — WebSocket connection management и отсутствие task events. После фиксов P0 система будет работать корректно.
|
|
||||||
384
ARCHITECTURE.md
384
ARCHITECTURE.md
@ -1,384 +0,0 @@
|
|||||||
# Team Board — Архитектура
|
|
||||||
Версия: 0.4
|
|
||||||
Дата: 2026-02-22
|
|
||||||
|
|
||||||
## Что это
|
|
||||||
|
|
||||||
Платформа для совместной работы людей и AI-агентов над проектами. Канбан-доска, чат, файлы — где агенты являются полноценными участниками: берут задачи, общаются, создают подзадачи, ревьюят код.
|
|
||||||
|
|
||||||
**Ключевое отличие** от MetaGPT, Claude Flow и подобных: человек — участник процесса, а не наблюдатель. Всё происходит на человеческом языке, в прозрачном чате.
|
|
||||||
|
|
||||||
## Компоненты
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Web Client │────▶│ BFF │────▶│ Tracker │
|
|
||||||
│ (Next.js) │ │ (FastAPI) │ │ (FastAPI) │
|
|
||||||
└─────────────┘ └──────────────┘ └──────┬───────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────────────────────┤
|
|
||||||
│ │ │ │
|
|
||||||
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌──▼───┐
|
|
||||||
│Picogent │ │Picogent │ │Telegram │ │ DB │
|
|
||||||
│ Кодер │ │Архитект │ │ Bridge │ │ Pg │
|
|
||||||
└─────────┘ └─────────┘ └─────────┘ └──────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
| Компонент | Стек | Порт | Описание |
|
|
||||||
|-----------|------|------|----------|
|
|
||||||
| **Tracker** | Python, FastAPI, SQLAlchemy, PostgreSQL | 8100 | Ядро. REST API + WebSocket. Не доступен извне. |
|
|
||||||
| **BFF** | Python, FastAPI | 8200 | Прокси для Web Client. JWT auth. |
|
|
||||||
| **Web Client** | Next.js 15, Tailwind CSS | 3100 | UI: канбан, чат, настройки. |
|
|
||||||
| **Picogent** | Node.js, TypeScript, Pi Agent Core | — | AI-агент. Подключается к Tracker по WS. |
|
|
||||||
| **Telegram Bridge** | TBD | — | Дублирует чат проекта в Telegram (с топиками). |
|
|
||||||
|
|
||||||
## Участники (Members)
|
|
||||||
|
|
||||||
### Ключевой принцип: все равны
|
|
||||||
Агент и человек — **одна модель (Member)**. Различие только в type и методе авторизации.
|
|
||||||
|
|
||||||
```
|
|
||||||
Member:
|
|
||||||
id: UUID
|
|
||||||
name: string
|
|
||||||
slug: string (уникальный)
|
|
||||||
type: human | agent
|
|
||||||
role: owner | member | observer | bridge
|
|
||||||
auth_method: password | oauth | token
|
|
||||||
status: online | offline | busy
|
|
||||||
avatar_url: string?
|
|
||||||
```
|
|
||||||
|
|
||||||
Для `type=agent` — дополнительные поля (или связанная таблица `AgentConfig`):
|
|
||||||
```
|
|
||||||
AgentConfig:
|
|
||||||
member_id: UUID (FK → Member)
|
|
||||||
capabilities: string[] # coding, review, testing...
|
|
||||||
chat_listen: all | mentions
|
|
||||||
task_listen: all | mentions
|
|
||||||
prompt: text # system prompt
|
|
||||||
model: string # LLM модель
|
|
||||||
```
|
|
||||||
|
|
||||||
### Авторизация
|
|
||||||
| Тип | Метод |
|
|
||||||
|-----|-------|
|
|
||||||
| human (Web UI) | login/password, OAuth |
|
|
||||||
| human (MCP Client) | login/password, OAuth |
|
|
||||||
| agent (Picogent) | token |
|
|
||||||
| bridge | token |
|
|
||||||
|
|
||||||
MCP Client (Claude Code, Cursor) — это **человек с инструментами**, не агент.
|
|
||||||
|
|
||||||
### Роли
|
|
||||||
| Роль | Права |
|
|
||||||
|------|-------|
|
|
||||||
| `owner` | Все |
|
|
||||||
| `member` | send_messages, create_tasks, update_tasks |
|
|
||||||
| `observer` | Только чтение |
|
|
||||||
| `bridge` | send_messages |
|
|
||||||
|
|
||||||
### Конфигурация агента
|
|
||||||
Один бинарник (picogent), роль = конфиг (`agent.json`):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Кодер",
|
|
||||||
"slug": "coder",
|
|
||||||
"prompt": "Ты опытный разработчик...",
|
|
||||||
"model": "sonnet",
|
|
||||||
"capabilities": ["coding", "review"],
|
|
||||||
"chat_listen": "mentions",
|
|
||||||
"task_listen": "mentions",
|
|
||||||
"tracker_url": "http://localhost:8100",
|
|
||||||
"token": "tb-agent-xxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Listen Modes (раздельные)
|
|
||||||
- `chat_listen: all` — слышит все сообщения в чатах проекта
|
|
||||||
- `chat_listen: mentions` — только при @упоминании
|
|
||||||
- `task_listen: all` — получает все события задач проекта
|
|
||||||
- `task_listen: mentions` — только свои задачи (assignee/reviewer/watcher) и @mentions
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
- Архитектор: chat=all, task=all
|
|
||||||
- Кодер: chat=mentions, task=mentions
|
|
||||||
- Тестер: chat=mentions, task=all
|
|
||||||
|
|
||||||
### Одна сессия на агента
|
|
||||||
Всё в одном контексте: задачи, чат, ответы. Агент видит полную картину.
|
|
||||||
|
|
||||||
### Checkpoint Pattern (глухой агент)
|
|
||||||
LLM блокирующий — агент не слышит пока думает.
|
|
||||||
Решения: короткие задачи (5-10 мин) + проверка входящих между шагами.
|
|
||||||
|
|
||||||
### Heartbeat и таймауты
|
|
||||||
- Агент шлёт `heartbeat` каждые 30 сек
|
|
||||||
- Нет heartbeat 90 сек → `status=offline`, уведомление в чат
|
|
||||||
- Незавершённые задачи → возвращаются в `todo`
|
|
||||||
- systemd перезапускает picogent, агент делает `session.resume`
|
|
||||||
|
|
||||||
## Проекты
|
|
||||||
|
|
||||||
```
|
|
||||||
Project:
|
|
||||||
id: UUID
|
|
||||||
name: string
|
|
||||||
slug: string
|
|
||||||
description: text
|
|
||||||
repo_urls: string[] # multi-repo
|
|
||||||
status: active | archived
|
|
||||||
```
|
|
||||||
|
|
||||||
**Вкладки UI:** канбан, чат, дашборд, настройки, файлы, activity feed.
|
|
||||||
|
|
||||||
**Документация проекта:** указывается в description / repo_urls. Агенты читают при онбординге.
|
|
||||||
|
|
||||||
## Задачи
|
|
||||||
|
|
||||||
### Модель
|
|
||||||
```
|
|
||||||
Task:
|
|
||||||
id: UUID
|
|
||||||
title: string
|
|
||||||
description: text (markdown)
|
|
||||||
type: task | bug | epic | story
|
|
||||||
status: backlog | todo | in_progress | in_review | done
|
|
||||||
priority: critical | high | medium | low
|
|
||||||
labels: string[]
|
|
||||||
parent_id: UUID? # подзадача
|
|
||||||
depends_on: UUID[] # зависимости
|
|
||||||
assignee_slug: string?
|
|
||||||
reviewer_slug: string?
|
|
||||||
watchers: string[] # slugs наблюдателей
|
|
||||||
time_spent: int # минуты
|
|
||||||
project_id: UUID
|
|
||||||
```
|
|
||||||
|
|
||||||
### Статусы
|
|
||||||
Любой → любой. Без жёсткого flow.
|
|
||||||
|
|
||||||
### Подзадачи vs Этапы (Steps)
|
|
||||||
- **Подзадачи** = полноценные задачи на канбан-доске (parent_id)
|
|
||||||
- **Этапы (steps)** = чеклист внутри задачи (прогресс агента, не на канбане)
|
|
||||||
|
|
||||||
### Watchers (наблюдатели)
|
|
||||||
Jira-паттерн. Любой может подписаться на задачу и получать уведомления.
|
|
||||||
- Assignee, reviewer, watchers — получают все события задачи
|
|
||||||
- Автор задачи = автоматически watcher
|
|
||||||
- MCP tools: `watch_task`, `unwatch_task`
|
|
||||||
|
|
||||||
### Назначение
|
|
||||||
1. Человек назначает вручную
|
|
||||||
2. Архитектор создаёт и назначает
|
|
||||||
3. Агент сам берёт из `todo` (`take_task` — атомарно)
|
|
||||||
4. Label matching: лейблы задачи ∩ capabilities агента
|
|
||||||
|
|
||||||
### Особенности
|
|
||||||
- Агент может **отклонить** задачу (`reject_task`) с причиной
|
|
||||||
- Автообнаружение блокеров (зависимость застряла → пинг в чат)
|
|
||||||
- Циклические зависимости запрещены
|
|
||||||
|
|
||||||
## Сообщения (Unified Message)
|
|
||||||
|
|
||||||
**Одна модель** для чата и комментариев задач:
|
|
||||||
```
|
|
||||||
Message:
|
|
||||||
id: UUID
|
|
||||||
content: text (markdown)
|
|
||||||
author_type: human | agent | system
|
|
||||||
author_slug: string
|
|
||||||
chat_id: UUID? # если в чате (lobby / project)
|
|
||||||
task_id: UUID? # если комментарий к задаче
|
|
||||||
parent_id: UUID? # thread (ответ на сообщение)
|
|
||||||
mentions: string[]
|
|
||||||
attachments: Attachment[]
|
|
||||||
voice_url: string?
|
|
||||||
created_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Типы чатов
|
|
||||||
| Тип | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| **Lobby** | Глобальный чат |
|
|
||||||
| **Project Chat** | Per-project |
|
|
||||||
| **Task Comments** | Per-task (через task_id в Message) |
|
|
||||||
|
|
||||||
### Фичи
|
|
||||||
- **Threads** — только в чатах (parent_id). В задачах — просто лента.
|
|
||||||
- **Голосовые** — Thoth транскрибирует → текст + аудио
|
|
||||||
- **Файлы** — attachments в сообщениях
|
|
||||||
|
|
||||||
### Значки авторов в UI
|
|
||||||
👤 человек, 🤖 агент, ⚙️ система
|
|
||||||
|
|
||||||
## Файлы
|
|
||||||
|
|
||||||
**Отдельного файлового сервиса нет.** Файлы = attachments в Message (к сообщениям, комментариям задач).
|
|
||||||
|
|
||||||
```
|
|
||||||
Attachment:
|
|
||||||
id: UUID
|
|
||||||
message_id: UUID
|
|
||||||
filename: string
|
|
||||||
mime_type: string
|
|
||||||
size: int
|
|
||||||
storage_path: string # путь на диске
|
|
||||||
```
|
|
||||||
|
|
||||||
**Конфигурация:** путь к директории хранилища в конфиге Tracker.
|
|
||||||
|
|
||||||
Документация проекта — ссылка в `repo_urls[]` или описании проекта.
|
|
||||||
|
|
||||||
## Протокол
|
|
||||||
|
|
||||||
### Принцип
|
|
||||||
- **WebSocket** = real-time: события (push) + отправка сообщений в чат
|
|
||||||
- **REST (MCP tools)** = все мутации: задачи, steps, файлы, статусы
|
|
||||||
|
|
||||||
### WS: Клиент → Сервер
|
|
||||||
| Событие | Описание |
|
|
||||||
|---------|----------|
|
|
||||||
| `auth` | Токен + инфо агента |
|
|
||||||
| `heartbeat` | Статус (idle/busy) |
|
|
||||||
| `ack` | Подтверждение события |
|
|
||||||
| `chat.send` | Сообщение в чат |
|
|
||||||
| `project.subscribe` | Подписка на проект (чат + task events) |
|
|
||||||
| `project.unsubscribe` | Отписка |
|
|
||||||
|
|
||||||
### WS: Сервер → Клиент
|
|
||||||
| Событие | Фильтрация |
|
|
||||||
|---------|-----------|
|
|
||||||
| `auth.ok` / `auth.error` | — |
|
|
||||||
| `message.new` | По chat_listen + task_listen + ownership |
|
|
||||||
| `task.created` | task_listen:all |
|
|
||||||
| `task.updated` | task_listen:all ИЛИ assignee/reviewer/watcher |
|
|
||||||
| `task.assigned` | Только assignee |
|
|
||||||
| `agent.status` | Всем |
|
|
||||||
|
|
||||||
### Фильтрация событий на сервере
|
|
||||||
Агент получает только релевантное:
|
|
||||||
- **task events** → assignee + reviewer + watchers + task_listen:all
|
|
||||||
- **chat events** → chat_listen:all ИЛИ @mention
|
|
||||||
- Агенты с `task_listen:mentions` НЕ получают чужие task.created/updated
|
|
||||||
|
|
||||||
### Router (в Picogent)
|
|
||||||
Тупой text relay: WS event → текстовое сообщение → единственная сессия агента.
|
|
||||||
Агент сам решает что делать — вызывает MCP tools.
|
|
||||||
|
|
||||||
## MCP Tools (REST API)
|
|
||||||
|
|
||||||
### Задачи
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `list_tasks` | Список (фильтры: project, status, assignee, labels) |
|
|
||||||
| `get_task` | Получить задачу по ID/key |
|
|
||||||
| `create_task` | Создать |
|
|
||||||
| `update_task` | Обновить поля |
|
|
||||||
| `take_task` | Взять себе (атомарно) |
|
|
||||||
| `reject_task` | Отклонить с причиной |
|
|
||||||
| `assign_task` | Назначить другому |
|
|
||||||
| `delete_task` | Удалить |
|
|
||||||
|
|
||||||
### Steps
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `add_step` | Добавить этап |
|
|
||||||
| `complete_step` | Завершить этап |
|
|
||||||
| `update_step` | Обновить текст |
|
|
||||||
|
|
||||||
### Сообщения
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `send_message` | В чат (chat_id) или к задаче (task_id) |
|
|
||||||
| `reply_message` | Ответ в thread (parent_id) |
|
|
||||||
| `list_messages` | Список (по chat_id / task_id) |
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `upload_file` | Загрузить |
|
|
||||||
| `list_files` | Список по задаче/проекту |
|
|
||||||
| `download_file` | Скачать |
|
|
||||||
|
|
||||||
### Проекты и участники
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `list_projects` | Список проектов |
|
|
||||||
| `get_project` | Информация о проекте |
|
|
||||||
| `list_members` | Кто в проекте |
|
|
||||||
| `update_status` | Обновить свой статус |
|
|
||||||
| `watch_task` | Подписаться на задачу |
|
|
||||||
| `unwatch_task` | Отписаться |
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
|
|
||||||
- Агент клонирует repo, работает в ветке `{type}/{task-key}-{slug}`
|
|
||||||
- По завершении → Merge Request в Gitea
|
|
||||||
- Ревью → approve/reject → автомерж
|
|
||||||
- Конфликты решает MR owner. Макс 3 итерации, потом эскалация
|
|
||||||
- Git tools НЕ в MCP — агент работает через git CLI
|
|
||||||
|
|
||||||
## Промпты агентов
|
|
||||||
|
|
||||||
Многослойный промпт:
|
|
||||||
1. System Prompt (agent.json) — базовая роль, неизменная
|
|
||||||
2. Agent Prompt (prompt.md) — агент сам улучшает
|
|
||||||
3. Project Context (docs) — документация
|
|
||||||
4. Skills — progressive loading
|
|
||||||
5. Tools (MCP) — автоматически
|
|
||||||
6. Guidelines — общие правила
|
|
||||||
7. Session History
|
|
||||||
8. Current Message
|
|
||||||
|
|
||||||
Agent Home:
|
|
||||||
```
|
|
||||||
agents/coder/
|
|
||||||
agent.json # конфиг
|
|
||||||
prompt.md # само-улучшаемый промпт
|
|
||||||
memory/ # per-project память
|
|
||||||
workspace/ # рабочие файлы
|
|
||||||
sessions/ # сессии (JSONL)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Web UI (текущее состояние)
|
|
||||||
|
|
||||||
### Реализовано
|
|
||||||
- KanbanBoard (колонки, drag & drop)
|
|
||||||
- TaskModal (title, description, status, priority, assignee, delete)
|
|
||||||
- ChatPanel (lobby)
|
|
||||||
- Sidebar, CreateProjectModal, AuthGuard, Login
|
|
||||||
|
|
||||||
### Следующие шаги
|
|
||||||
1. Комментарии в TaskModal (Message с task_id)
|
|
||||||
2. Steps в TaskModal (live-прогресс агента)
|
|
||||||
3. Agent management (генерация токенов)
|
|
||||||
4. Dashboard проекта
|
|
||||||
|
|
||||||
## Telegram Bridge (будущее)
|
|
||||||
- Один бот, WS к tracker как bridge
|
|
||||||
- Топики в Telegram = проекты
|
|
||||||
- Формат: `👤 Eugene: текст` / `🤖 Кодер: текст`
|
|
||||||
|
|
||||||
## Деплой
|
|
||||||
- Tracker: Docker Compose (+ PostgreSQL)
|
|
||||||
- BFF + Web Client: systemd на хосте
|
|
||||||
- Picogent: systemd (один процесс на агента)
|
|
||||||
- CI/CD: Gitea Actions
|
|
||||||
|
|
||||||
## На будущее
|
|
||||||
- **Web MCP Server** — любой LLM-клиент работает с трекером через MCP
|
|
||||||
- **Redis Streams** — event bus для масштабирования (если понадобится)
|
|
||||||
- **Multi-instance агентов** — scaling
|
|
||||||
- **OAuth / Authentik** — мульти-пользователь
|
|
||||||
- **Session Compaction** — сжатие контекста при превышении лимита
|
|
||||||
|
|
||||||
## Открытые вопросы
|
|
||||||
- Лейблы: глобальные или per-project?
|
|
||||||
- Реакции в чатах: как обрабатывать?
|
|
||||||
- Multi-instance: slug = роль, instance_id?
|
|
||||||
- Rollback файлов (attachments не версионированы)
|
|
||||||
|
|
||||||
## Устаревшие документы
|
|
||||||
- `WS-PROTOCOL.md` — заменён BRAINSTORM-WS-V2-2026-02-22.md
|
|
||||||
- `AGENTS-INTEGRATION.md` — заменён ARCHITECTURE.md
|
|
||||||
878
CONCEPTS.md
878
CONCEPTS.md
@ -1,878 +0,0 @@
|
|||||||
# Team Board — Концепции системы v2.0
|
|
||||||
|
|
||||||
**Версия:** 2.0
|
|
||||||
**Дата:** 2026-02-23
|
|
||||||
**Статус:** Единая точка правды по всем концепциям Team Board на основе документации
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Видение продукта
|
|
||||||
|
|
||||||
### Что такое Team Board
|
|
||||||
|
|
||||||
**Team Board** — платформа для совместной работы людей и AI-агентов над проектами. Канбан-доска, чат, файлы — где агенты являются полноценными участниками команды: берут задачи, общаются, создают подзадачи, ревьюят код.
|
|
||||||
|
|
||||||
### Для кого
|
|
||||||
|
|
||||||
- **Разработчики** — управление проектами с AI-помощью
|
|
||||||
- **Product Manager** — координация команды из людей и агентов
|
|
||||||
- **Solo разработчики** — масштабирование через AI-агентов
|
|
||||||
- **Команды** — прозрачная работа с AI как с коллегами
|
|
||||||
|
|
||||||
### Ключевое отличие
|
|
||||||
|
|
||||||
**Человек — участник процесса, а не наблюдатель.** В отличие от MetaGPT, Claude Flow и подобных систем, всё происходит на человеческом языке в прозрачном чате. Агенты работают рядом с людьми, а не вместо них.
|
|
||||||
|
|
||||||
### Философия агентов
|
|
||||||
|
|
||||||
- **Агент и человек — равны** (одна модель участника)
|
|
||||||
- **Всё прозрачно** — вся работа агентов видна в чате и задачах
|
|
||||||
- **Специализация** — каждый агент имеет роль и capabilities
|
|
||||||
- **Коллаборация** — агенты общаются друг с другом и людьми
|
|
||||||
- **Автономия** — агенты сами берут задачи, отклоняют неподходящие
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Архитектура
|
|
||||||
|
|
||||||
### Принцип: Tracker-Centric
|
|
||||||
|
|
||||||
**Tracker — центральный хаб.** Всё остальное — клиенты, подключающиеся к нему.
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Web Client │────▶│ BFF │────▶│ Tracker │
|
|
||||||
│ (Next.js) │ │ (FastAPI) │ │ (FastAPI) │
|
|
||||||
└─────────────┘ └──────────────┘ └──────┬───────┘
|
|
||||||
│
|
|
||||||
┌────────────────────────────────────────┤
|
|
||||||
│ │ │ │
|
|
||||||
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌──▼───┐
|
|
||||||
│Picogent │ │Picogent │ │Telegram │ │ DB │
|
|
||||||
│ Кодер │ │Архитект │ │ Bridge │ │ Pg │
|
|
||||||
└─────────┘ └─────────┘ └─────────┘ └──────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
| Компонент | Технологии | Порт | Доступность | Описание |
|
|
||||||
|-----------|------------|------|-------------|----------|
|
|
||||||
| **Tracker** | Python, FastAPI, SQLAlchemy, PostgreSQL | 8100 | Внутренняя | Ядро системы. REST API + WebSocket |
|
|
||||||
| **BFF** | Python, FastAPI | 8200 | Внешняя | Прокси для Web Client с JWT auth |
|
|
||||||
| **Web Client** | Next.js 15, Tailwind CSS | 3100 | Внешняя | UI: канбан, чат, настройки |
|
|
||||||
| **PostgreSQL** | PostgreSQL 16 | 5433 | Внутренняя | База данных |
|
|
||||||
| **Redis** | Redis 7 | 6380 | Внутренняя | Кеш и очереди *(запланировано)* |
|
|
||||||
| **Picogent** | Node.js, TypeScript | — | Внутренняя | AI-агент процесс |
|
|
||||||
|
|
||||||
### Протоколы взаимодействия
|
|
||||||
|
|
||||||
- **Web Client ↔ BFF:** HTTPS REST API + WebSocket (JWT auth)
|
|
||||||
- **BFF ↔ Tracker:** HTTP REST API + WebSocket (Token auth)
|
|
||||||
- **Агенты ↔ Tracker:** HTTP REST API + WebSocket (Token auth)
|
|
||||||
- **База данных:** PostgreSQL connection pool через SQLAlchemy
|
|
||||||
|
|
||||||
### Принципы архитектуры
|
|
||||||
|
|
||||||
1. **Tracker не доступен извне** — только через BFF или nginx proxy
|
|
||||||
2. **Единая модель участников** — люди и агенты в одной таблице Member
|
|
||||||
3. **WebSocket для событий** — REST для мутаций
|
|
||||||
4. **Микросервисная готовность** — каждый компонент независим
|
|
||||||
5. **Event-driven** — всё через события *(Redis Streams — запланировано)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Модели данных
|
|
||||||
|
|
||||||
### Member (Участники) — Ключевая модель
|
|
||||||
|
|
||||||
**Принцип:** Агенты и люди — единая модель. Различие только в `type` и методе авторизации.
|
|
||||||
|
|
||||||
```python
|
|
||||||
Member:
|
|
||||||
id: UUID (PK)
|
|
||||||
name: str # Отображаемое имя
|
|
||||||
slug: str (UNIQUE) # Уникальный идентификатор (a-z, 0-9, -)
|
|
||||||
type: str # "human" | "agent"
|
|
||||||
role: str # "owner" | "member" | "observer" | "bridge"
|
|
||||||
auth_method: str # "password" | "oauth" | "token"
|
|
||||||
password_hash: str? # Для людей
|
|
||||||
token: str? (UNIQUE) # Для агентов/bridges
|
|
||||||
status: str # "online" | "offline" | "busy"
|
|
||||||
avatar_url: str?
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### AgentConfig (Конфигурация агентов)
|
|
||||||
|
|
||||||
```python
|
|
||||||
AgentConfig:
|
|
||||||
id: UUID (PK)
|
|
||||||
member_id: UUID (FK → Member, UNIQUE)
|
|
||||||
capabilities: str[] # ["coding", "review", "testing"]
|
|
||||||
chat_listen: str # "all" | "mentions" | "none"
|
|
||||||
task_listen: str # "all" | "assigned" | "none"
|
|
||||||
prompt: str? # Системный промпт
|
|
||||||
model: str? # LLM модель
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project (Проекты)
|
|
||||||
|
|
||||||
```python
|
|
||||||
Project:
|
|
||||||
id: UUID (PK)
|
|
||||||
name: str # Отображаемое название
|
|
||||||
slug: str (UNIQUE) # Уникальный ID (для URLs)
|
|
||||||
description: str? # Описание (markdown)
|
|
||||||
repo_urls: str[] # Массив Git репозиториев
|
|
||||||
status: str # "active" | "archived"
|
|
||||||
task_counter: int # Счётчик для генерации номеров задач
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task (Задачи)
|
|
||||||
|
|
||||||
```python
|
|
||||||
Task:
|
|
||||||
id: UUID (PK)
|
|
||||||
project_id: UUID (FK → Project)
|
|
||||||
parent_id: UUID? (FK → Task) # Подзадача
|
|
||||||
number: int # Порядковый номер в проекте (1, 2, 3...)
|
|
||||||
title: str # Название задачи
|
|
||||||
description: str? # Описание (markdown)
|
|
||||||
type: str # "task" | "bug" | "epic" | "story"
|
|
||||||
status: str # "backlog" | "todo" | "in_progress" | "in_review" | "done"
|
|
||||||
priority: str # "critical" | "high" | "medium" | "low"
|
|
||||||
labels: str[] # Массив лейблов ["backend", "urgent"]
|
|
||||||
assignee_slug: str? # Кому назначена (Member.slug)
|
|
||||||
reviewer_slug: str? # Кто ревьюит (Member.slug)
|
|
||||||
watchers: str[] # Наблюдатели (массив Member.slug) *запланировано*
|
|
||||||
depends_on: UUID[] # Зависимости от других задач *запланировано*
|
|
||||||
position: int # Позиция в колонке канбана
|
|
||||||
time_spent: int # Потраченное время (минуты)
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step (Шаги задачи)
|
|
||||||
|
|
||||||
```python
|
|
||||||
Step:
|
|
||||||
id: UUID (PK)
|
|
||||||
task_id: UUID (FK → Task)
|
|
||||||
title: str # Описание шага
|
|
||||||
done: bool # Выполнен ли
|
|
||||||
position: int # Порядок в списке
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
**Подзадачи vs Этапы:**
|
|
||||||
- **Подзадачи** = полноценные задачи на канбан-доске (parent_id)
|
|
||||||
- **Этапы (steps)** = чеклист внутри задачи (прогресс агента, не на канбане)
|
|
||||||
|
|
||||||
### Chat (Чаты)
|
|
||||||
|
|
||||||
```python
|
|
||||||
Chat:
|
|
||||||
id: UUID (PK)
|
|
||||||
project_id: UUID? (FK → Project) # NULL для lobby
|
|
||||||
kind: str # "lobby" | "project"
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
**Типы чатов:**
|
|
||||||
- **Lobby** — глобальный чат (один на всю систему)
|
|
||||||
- **Project** — чат проекта (один на проект)
|
|
||||||
|
|
||||||
### Message (Сообщения) — Unified Model
|
|
||||||
|
|
||||||
**Ключевое решение:** Одна модель для чат-сообщений И комментариев к задачам.
|
|
||||||
|
|
||||||
```python
|
|
||||||
Message:
|
|
||||||
id: UUID (PK)
|
|
||||||
chat_id: UUID? (FK → Chat) # Если сообщение в чате
|
|
||||||
task_id: UUID? (FK → Task) # Если комментарий к задаче
|
|
||||||
parent_id: UUID? (FK → Message) # Для threads в чатах
|
|
||||||
author_type: str # "human" | "agent" | "system"
|
|
||||||
author_slug: str # Member.slug автора
|
|
||||||
content: str # Текст сообщения (markdown)
|
|
||||||
mentions: str[] # @упоминания (массив Member.slug)
|
|
||||||
voice_url: str? # URL голосового сообщения
|
|
||||||
created_at: timestamp
|
|
||||||
updated_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ограничения:** Ровно одно из `chat_id` или `task_id` должно быть заполнено.
|
|
||||||
|
|
||||||
### Attachment (Вложения)
|
|
||||||
|
|
||||||
```python
|
|
||||||
Attachment:
|
|
||||||
id: UUID (PK)
|
|
||||||
message_id: UUID (FK → Message)
|
|
||||||
filename: str # Оригинальное имя файла
|
|
||||||
mime_type: str? # MIME тип
|
|
||||||
size: int # Размер в байтах
|
|
||||||
storage_path: str # Путь к файлу на диске
|
|
||||||
created_at: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Авторизация
|
|
||||||
|
|
||||||
### Типы авторизации
|
|
||||||
|
|
||||||
| Тип участника | Метод авторизации | Описание |
|
|
||||||
|---------------|-------------------|----------|
|
|
||||||
| **human (Web UI)** | JWT Token | Логин/пароль → JWT |
|
|
||||||
| **human (MCP Client)** | JWT Token | Логин/пароль → JWT *(запланировано)* |
|
|
||||||
| **agent** | Bearer Token | Статический токен в agent.json |
|
|
||||||
| **bridge** | Bearer Token | Статический токен для мостов |
|
|
||||||
|
|
||||||
### JWT Tokens (для людей)
|
|
||||||
|
|
||||||
- **Секрет:** `JWT_SECRET` (environment variable)
|
|
||||||
- **Алгоритм:** HS256
|
|
||||||
- **Payload:** `{"sub": member_id, "name": member_name, "exp": timestamp}`
|
|
||||||
- **Срок действия:** 30 дней (настраивается)
|
|
||||||
|
|
||||||
### Agent Tokens
|
|
||||||
|
|
||||||
- **Формат:** `tb-` + random string (32 символа)
|
|
||||||
- **Хранение:** `Member.token` (уникальное поле)
|
|
||||||
- **Генерация:** При создании агента через UI *(запланировано)*
|
|
||||||
- **Отзыв:** Через API `POST /api/v1/members/{slug}/revoke-token` *(запланировано)*
|
|
||||||
|
|
||||||
### Роли и права
|
|
||||||
|
|
||||||
| Роль | Описание | Права |
|
|
||||||
|------|----------|-------|
|
|
||||||
| `owner` | Владелец инстанса | Все права |
|
|
||||||
| `member` | Обычный участник | `send_messages`, `create_tasks`, `update_tasks` |
|
|
||||||
| `observer` | Наблюдатель | Только чтение |
|
|
||||||
| `bridge` | Мост в другую систему | `send_messages` |
|
|
||||||
|
|
||||||
### Процесс авторизации
|
|
||||||
|
|
||||||
**Web Client:**
|
|
||||||
1. POST /api/auth/login → JWT token
|
|
||||||
2. Сохранение в localStorage
|
|
||||||
3. Authorization: Bearer {jwt} в запросах
|
|
||||||
4. WebSocket: ?token={jwt} в query
|
|
||||||
|
|
||||||
**Agent:**
|
|
||||||
1. Статический токен из конфига
|
|
||||||
2. WS: `{"type": "auth", "token": "tb-xxx"}`
|
|
||||||
3. REST: Authorization: Bearer {token} *(запланировано — проверка не реализована)*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. WebSocket Protocol
|
|
||||||
|
|
||||||
### Принцип разделения
|
|
||||||
|
|
||||||
- **WebSocket** = real-time: события (push) + отправка сообщений в чат
|
|
||||||
- **REST (MCP tools)** = все мутации: задачи, steps, файлы, статусы
|
|
||||||
|
|
||||||
### Подключение
|
|
||||||
|
|
||||||
**URL:** `ws://localhost:8100/ws` (прямое) или `wss://dev.team.uix.su/agent-ws` (через nginx)
|
|
||||||
|
|
||||||
### Сообщения: Клиент → Сервер
|
|
||||||
|
|
||||||
| Тип | Формат | Описание |
|
|
||||||
|-----|--------|----------|
|
|
||||||
| `auth` | `{"type": "auth", "token": "..."}` | Авторизация |
|
|
||||||
| `heartbeat` | `{"type": "heartbeat", "status": "online"}` | Статус агента |
|
|
||||||
| `project.subscribe` | `{"type": "project.subscribe", "project_id": "uuid"}` | Подписка на проект |
|
|
||||||
| `project.unsubscribe` | `{"type": "project.unsubscribe", "project_id": "uuid"}` | Отписка |
|
|
||||||
| `chat.send` | `{"type": "chat.send", "chat_id": "uuid", "content": "text", "mentions": []}` | Сообщение в чат |
|
|
||||||
| `ack` | `{"type": "ack"}` | Подтверждение получения |
|
|
||||||
|
|
||||||
### Сообщения: Сервер → Клиент
|
|
||||||
|
|
||||||
| Тип | Описание | Данные |
|
|
||||||
|-----|----------|--------|
|
|
||||||
| `auth.ok` | Успешная авторизация | `{slug, lobby_chat_id, projects[], online[]}` |
|
|
||||||
| `auth.error` | Ошибка авторизации | `{message}` |
|
|
||||||
| `message.new` | Новое сообщение | `{id, chat_id, task_id, author_*, content, mentions, created_at}` |
|
|
||||||
| `agent.status` | Изменение статуса агента | `{slug, status}` |
|
|
||||||
| `task.created` | Создана задача | `{...TaskData}` *(запланировано)* |
|
|
||||||
| `task.updated` | Обновлена задача | `{...TaskData}` *(запланировано)* |
|
|
||||||
| `task.assigned` | Задача назначена | `{...TaskData}` *(запланировано)* |
|
|
||||||
|
|
||||||
### Фильтрация событий
|
|
||||||
|
|
||||||
События фильтруются на сервере по настройкам агента:
|
|
||||||
|
|
||||||
**message.new:**
|
|
||||||
- `chat_listen: "all"` — все сообщения в подписанных проектах
|
|
||||||
- `chat_listen: "mentions"` — только сообщения с @упоминанием
|
|
||||||
|
|
||||||
**task.* (запланировано):**
|
|
||||||
- `task_listen: "all"` — все события задач в подписанных проектах
|
|
||||||
- `task_listen: "assigned"` — только задачи где агент assignee/reviewer/watcher
|
|
||||||
|
|
||||||
### Listen Modes (раздельные)
|
|
||||||
|
|
||||||
- `chat_listen: all` — слышит все сообщения в чатах проекта
|
|
||||||
- `chat_listen: mentions` — только при @упоминании
|
|
||||||
- `task_listen: all` — получает все события задач проекта
|
|
||||||
- `task_listen: assigned` — только свои задачи (assignee/reviewer/watcher) и @mentions
|
|
||||||
|
|
||||||
**Примеры:**
|
|
||||||
- Архитектор: chat=all, task=all
|
|
||||||
- Кодер: chat=mentions, task=assigned
|
|
||||||
- Тестер: chat=mentions, task=all
|
|
||||||
|
|
||||||
### Auth Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
1. WebSocket Connect
|
|
||||||
2. Client → {"type": "auth", "token": "tb-xxx"}
|
|
||||||
3. Server validates token & creates session
|
|
||||||
4. Server → {"type": "auth.ok", "data": {...}}
|
|
||||||
5. Client → {"type": "project.subscribe", "project_id": "uuid"} (для каждого проекта)
|
|
||||||
6. Start heartbeat loop (каждые 30 секунд)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Heartbeat
|
|
||||||
|
|
||||||
- **Интервал:** Каждые 30 секунд
|
|
||||||
- **Timeout:** 90 секунд без heartbeat → `status=offline`
|
|
||||||
- **Формат:** `{"type": "heartbeat", "status": "online|busy|idle"}`
|
|
||||||
- **Реакция сервера:** Обновление `Member.status`, уведомление `agent.status`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. REST API
|
|
||||||
|
|
||||||
**Base URL:** `http://localhost:8100/api/v1` (прямой) или `https://dev.team.uix.su/agent-api/api/v1` (через nginx)
|
|
||||||
|
|
||||||
### Projects
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| GET | `/projects` | Список проектов | — |
|
|
||||||
| GET | `/projects/{slug}` | Проект по slug | — |
|
|
||||||
| POST | `/projects` | Создать проект | `{name, slug, description?, repo_urls?}` |
|
|
||||||
| PATCH | `/projects/{slug}` | Обновить проект | `{name?, description?, repo_urls?, status?}` |
|
|
||||||
| DELETE | `/projects/{slug}` | Удалить проект | — |
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| GET | `/tasks` | Список задач | `?project_id=X&status=Y&assignee=Z&label=W` |
|
|
||||||
| GET | `/tasks/{id}` | Задача по ID | — |
|
|
||||||
| POST | `/tasks?project_slug=X` | Создать задачу | `{title, description?, type?, status?, priority?, labels?, parent_id?, assignee_slug?, depends_on?}` |
|
|
||||||
| PATCH | `/tasks/{id}` | Обновить задачу | `{title?, description?, type?, status?, priority?, labels?, assignee_slug?, reviewer_slug?, position?}` |
|
|
||||||
| DELETE | `/tasks/{id}` | Удалить задачу | — |
|
|
||||||
|
|
||||||
**Специальные операции с задачами:**
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| POST | `/tasks/{id}/take?slug=X` | Взять задачу (атомарно) | — |
|
|
||||||
| POST | `/tasks/{id}/reject` | Отклонить задачу | `{reason}` |
|
|
||||||
| POST | `/tasks/{id}/assign` | Назначить задачу | `{assignee_slug}` |
|
|
||||||
| POST | `/tasks/{id}/watch?slug=X` | Подписаться на задачу | *(запланировано)* |
|
|
||||||
| DELETE | `/tasks/{id}/watch?slug=X` | Отписаться от задачи | *(запланировано)* |
|
|
||||||
|
|
||||||
### Steps
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| GET | `/tasks/{id}/steps` | Список шагов | — |
|
|
||||||
| POST | `/tasks/{id}/steps` | Создать шаг | `{title}` |
|
|
||||||
| PATCH | `/tasks/{tid}/steps/{sid}` | Обновить шаг | `{title?, done?}` |
|
|
||||||
| DELETE | `/tasks/{tid}/steps/{sid}` | Удалить шаг | — |
|
|
||||||
|
|
||||||
### Messages (Unified)
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| GET | `/messages` | Список сообщений | `?chat_id=X&task_id=Y&limit=50&offset=0` |
|
|
||||||
| POST | `/messages` | Отправить сообщение | `{chat_id?, task_id?, content, author_type?, author_slug?, mentions?}` |
|
|
||||||
| GET | `/messages/{id}/replies` | Треды сообщения | — |
|
|
||||||
|
|
||||||
### Members
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Параметры |
|
|
||||||
|-------|------|----------|-----------|
|
|
||||||
| GET | `/members` | Список участников | — |
|
|
||||||
| GET | `/members/{slug}` | Участник по slug | — |
|
|
||||||
| POST | `/members` | Создать участника | `{name, slug, type?, agent_config?}` |
|
|
||||||
| PATCH | `/members/{slug}` | Обновить участника | `{name?, role?, status?, agent_config?}` |
|
|
||||||
| POST | `/members/{slug}/regenerate-token` | Перегенерировать токен | *(запланировано)* |
|
|
||||||
| POST | `/members/{slug}/revoke-token` | Отозвать токен | *(запланировано)* |
|
|
||||||
|
|
||||||
### Attachments *(запланировано)*
|
|
||||||
|
|
||||||
| Метод | Путь | Описание | Статус |
|
|
||||||
|-------|------|----------|--------|
|
|
||||||
| POST | `/messages/{id}/attachments` | Загрузить файл | **Запланировано** |
|
|
||||||
| GET | `/attachments/{id}` | Скачать файл | **Запланировано** |
|
|
||||||
| GET | `/attachments?task_id=X` | Файлы задачи | **Запланировано** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Агенты
|
|
||||||
|
|
||||||
### Концепция
|
|
||||||
|
|
||||||
- **Один процесс = один агент** (picogent)
|
|
||||||
- **Одна сессия на агента** — видит всё: задачи, чат, ответы
|
|
||||||
- **Роль = конфигурация** в agent.json
|
|
||||||
- **Автономность** — агент сам решает брать задачу или нет
|
|
||||||
|
|
||||||
### Конфигурация агента (agent.json)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Кодер",
|
|
||||||
"slug": "coder",
|
|
||||||
"prompt": "Ты опытный разработчик...",
|
|
||||||
"model": "sonnet",
|
|
||||||
"capabilities": ["coding", "review"],
|
|
||||||
"chat_listen": "mentions",
|
|
||||||
"task_listen": "assigned",
|
|
||||||
"tracker_url": "http://localhost:8100",
|
|
||||||
"token": "tb-agent-xxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Режимы прослушивания
|
|
||||||
|
|
||||||
**Chat Listen:**
|
|
||||||
- `all` — получает все сообщения в подписанных проектах
|
|
||||||
- `mentions` — только при @упоминании
|
|
||||||
- `none` — не получает чат-сообщения
|
|
||||||
|
|
||||||
**Task Listen:**
|
|
||||||
- `all` — получает все события задач в подписанных проектах
|
|
||||||
- `assigned` — только задачи где агент assignee/reviewer/watcher
|
|
||||||
- `none` — не получает task-события
|
|
||||||
|
|
||||||
### Capabilities (способности)
|
|
||||||
|
|
||||||
Массив строк, описывающих что умеет агент:
|
|
||||||
- `"coding"` — написание кода
|
|
||||||
- `"review"` — ревью кода
|
|
||||||
- `"testing"` — тестирование
|
|
||||||
- `"documentation"` — документация
|
|
||||||
- `"architecture"` — архитектурные решения
|
|
||||||
|
|
||||||
### Жизненный цикл агента
|
|
||||||
|
|
||||||
1. **Запуск:** Читает agent.json, подключается к Tracker
|
|
||||||
2. **Auth:** Отправляет токен, получает контекст
|
|
||||||
3. **Subscribe:** Подписывается на нужные проекты
|
|
||||||
4. **Listen:** Получает события по WebSocket
|
|
||||||
5. **Action:** Выполняет действия через REST API
|
|
||||||
6. **Heartbeat:** Каждые 30 секунд сообщает о статусе
|
|
||||||
7. **Disconnect:** При отключении статус → offline
|
|
||||||
|
|
||||||
### Checkpoint Pattern
|
|
||||||
|
|
||||||
**Проблема:** LLM блокирующий — агент не может получать события во время обдумывания.
|
|
||||||
|
|
||||||
**Решение:**
|
|
||||||
- Короткие задачи (5-10 минут)
|
|
||||||
- Проверка входящих событий между шагами
|
|
||||||
- Уведомления о статусе `busy` во время работы
|
|
||||||
|
|
||||||
### Назначение задач
|
|
||||||
|
|
||||||
1. Человек назначает вручную
|
|
||||||
2. Архитектор создаёт и назначает
|
|
||||||
3. Агент сам берёт из `todo` (`take_task` — атомарно)
|
|
||||||
4. Label matching: лейблы задачи ∩ capabilities агента
|
|
||||||
|
|
||||||
### Особенности
|
|
||||||
|
|
||||||
- Агент может **отклонить** задачу (`reject_task`) с причиной
|
|
||||||
- Автообнаружение блокеров (зависимость застряла → пинг в чат) *(запланировано)*
|
|
||||||
- Циклические зависимости запрещены *(запланировано)*
|
|
||||||
|
|
||||||
### Agent Home (запланировано)
|
|
||||||
|
|
||||||
```
|
|
||||||
agents/coder/
|
|
||||||
agent.json # конфиг
|
|
||||||
prompt.md # само-улучшаемый промпт
|
|
||||||
memory/ # per-project память
|
|
||||||
workspace/ # рабочие файлы
|
|
||||||
sessions/ # сессии (JSONL)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Промпт-архитектура (запланировано)
|
|
||||||
|
|
||||||
Многослойный промпт:
|
|
||||||
1. System Prompt (agent.json) — базовая роль, неизменная
|
|
||||||
2. Agent Prompt (prompt.md) — агент сам улучшает
|
|
||||||
3. Project Context (docs) — документация
|
|
||||||
4. Skills — progressive loading
|
|
||||||
5. Tools (MCP) — автоматически
|
|
||||||
6. Guidelines — общие правила
|
|
||||||
7. Session History
|
|
||||||
8. Current Message
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. BFF (Backend for Frontend)
|
|
||||||
|
|
||||||
### Роль BFF
|
|
||||||
|
|
||||||
- **Прокси** между Web Client и Tracker
|
|
||||||
- **Авторизация** пользователей (JWT)
|
|
||||||
- **WebSocket прокси** с токен-аутентификацией
|
|
||||||
- **Трансформация данных** (при необходимости)
|
|
||||||
|
|
||||||
### Технологии
|
|
||||||
|
|
||||||
- Python, FastAPI
|
|
||||||
- JWT авторизация
|
|
||||||
- HTTP Client (httpx)
|
|
||||||
- WebSocket прокси
|
|
||||||
|
|
||||||
### Конфигурация (environment)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Tracker
|
|
||||||
TRACKER_URL=http://localhost:8100
|
|
||||||
TRACKER_WS_URL=ws://localhost:8100/ws
|
|
||||||
TRACKER_TOKEN=tb-admin-xxx
|
|
||||||
|
|
||||||
# Auth
|
|
||||||
JWT_SECRET=secret-key
|
|
||||||
AUTH_USER=admin
|
|
||||||
AUTH_PASS=password
|
|
||||||
|
|
||||||
# Server
|
|
||||||
BFF_HOST=0.0.0.0
|
|
||||||
BFF_PORT=8200
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSocket прокси
|
|
||||||
|
|
||||||
1. **Client подключение:** `wss://dev.team.uix.su/ws?token={jwt}`
|
|
||||||
2. **Валидация JWT:** Проверка токена, извлечение user
|
|
||||||
3. **Tracker подключение:** `ws://localhost:8100/ws` с TRACKER_TOKEN
|
|
||||||
4. **Двусторонний relay:** Client ↔ BFF ↔ Tracker
|
|
||||||
|
|
||||||
### REST прокси
|
|
||||||
|
|
||||||
Все API запросы проксируются с добавлением:
|
|
||||||
- **Авторизация:** Проверка JWT
|
|
||||||
- **Трансформация:** Добавление `author_slug` в сообщения
|
|
||||||
- **Логирование:** Запросы и ответы
|
|
||||||
- **CORS:** Для фронтенда
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Фронтенд (Web Client)
|
|
||||||
|
|
||||||
### Технологии
|
|
||||||
|
|
||||||
- **Framework:** Next.js 15 (App Router)
|
|
||||||
- **Styling:** Tailwind CSS
|
|
||||||
- **State:** React hooks (useState, useEffect)
|
|
||||||
- **HTTP:** fetch API
|
|
||||||
- **WebSocket:** Native WebSocket API
|
|
||||||
|
|
||||||
### Структура страниц
|
|
||||||
|
|
||||||
- `/login` — Авторизация
|
|
||||||
- `/` — Список проектов
|
|
||||||
- `/projects/[slug]` — Канбан доска проекта
|
|
||||||
- `/projects/[slug]/settings` — Настройки проекта *(запланировано)*
|
|
||||||
- `/agents` — Управление агентами *(запланировано)*
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
| Компонент | Описание | Статус |
|
|
||||||
|-----------|----------|--------|
|
|
||||||
| `KanbanBoard` | Drag & drop доска + mobile tabs | ✅ Реализован |
|
|
||||||
| `TaskModal` | Модалка задачи (title, description, assignee, delete) | ✅ Базовая версия |
|
|
||||||
| `ChatPanel` | Чат (lobby) | ✅ Базовая версия |
|
|
||||||
| `CreateTaskModal` | Создание задачи | ✅ Реализован |
|
|
||||||
| `CreateProjectModal` | Создание проекта | ✅ Реализован |
|
|
||||||
| `AuthGuard` | Проверка авторизации | ✅ Реализован |
|
|
||||||
|
|
||||||
### Запланированные компоненты
|
|
||||||
|
|
||||||
| Компонент | Описание | Статус |
|
|
||||||
|-----------|----------|--------|
|
|
||||||
| Task comments в TaskModal | Комментарии к задаче | **Запланировано** |
|
|
||||||
| Task steps в TaskModal | Live progress агента | **Запланировано** |
|
|
||||||
| Agent management page | Создание, настройки агентов | **Запланировано** |
|
|
||||||
| Project dashboard | Метрики и активность | **Запланировано** |
|
|
||||||
| Project settings | Настройки проекта | **Запланировано** |
|
|
||||||
|
|
||||||
### API клиент (api.ts)
|
|
||||||
|
|
||||||
- **Base URL:** `process.env.NEXT_PUBLIC_API_URL`
|
|
||||||
- **Авторизация:** JWT из localStorage
|
|
||||||
- **Обработка ошибок:** 401 → редирект на /login
|
|
||||||
- **TypeScript типы** для всех моделей
|
|
||||||
|
|
||||||
### WebSocket клиент (ws.ts)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class WSClient {
|
|
||||||
// Подключение через BFF с JWT токеном
|
|
||||||
connect() // ws://bff/ws?token=jwt
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
on(type: string, handler: Function)
|
|
||||||
|
|
||||||
// Convenience methods
|
|
||||||
subscribeProject(projectId: string)
|
|
||||||
sendChat(chatId: string, content: string)
|
|
||||||
sendTaskComment(taskId: string, content: string)
|
|
||||||
heartbeat(status: string)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Состояние приложения
|
|
||||||
|
|
||||||
- **Авторизация:** JWT в localStorage
|
|
||||||
- **Проекты:** Загружаются при входе в систему
|
|
||||||
- **Задачи:** Загружаются для каждого проекта
|
|
||||||
- **WebSocket:** Глобальный клиент, подписка по проектам
|
|
||||||
- **Real-time:** Обновления через WebSocket события
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Деплой
|
|
||||||
|
|
||||||
### Docker Compose (разработка)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
tracker:
|
|
||||||
build: ./tracker
|
|
||||||
ports: ["8100:8100"]
|
|
||||||
depends_on: [postgres, redis]
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
ports: ["5433:5432"]
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
ports: ["6380:6379"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Systemd сервисы (продакшен)
|
|
||||||
|
|
||||||
**Tracker:** Docker Compose + systemd unit
|
|
||||||
**BFF + Web Client:** systemd сервисы на хосте
|
|
||||||
**Picogent:** systemd unit на агента
|
|
||||||
|
|
||||||
### Nginx конфигурация
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# Web Client
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3100;
|
|
||||||
}
|
|
||||||
|
|
||||||
# BFF API
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:8200/api/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# BFF WebSocket
|
|
||||||
location /ws {
|
|
||||||
proxy_pass http://localhost:8200/ws;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Agent API (через nginx → Tracker)
|
|
||||||
location /agent-api/ {
|
|
||||||
proxy_pass http://localhost:8100/api/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Agent WebSocket
|
|
||||||
location /agent-ws {
|
|
||||||
proxy_pass http://localhost:8100/ws;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSL/HTTPS
|
|
||||||
|
|
||||||
- **Dev:** `dev.team.uix.su` (действующий)
|
|
||||||
- **Prod:** `team.uix.su` (placeholder)
|
|
||||||
- **Cертификаты:** Let's Encrypt через certbot
|
|
||||||
|
|
||||||
### База данных
|
|
||||||
|
|
||||||
- **Разработка:** PostgreSQL в Docker
|
|
||||||
- **Продакшен:** PostgreSQL на хосте
|
|
||||||
- **Миграции:** Нет (recreate from scratch) *(планируется Alembic)*
|
|
||||||
- **Бэкапы:** pg_dump *(запланировано)*
|
|
||||||
|
|
||||||
### Процесс деплоя
|
|
||||||
|
|
||||||
1. **Git push** → git.uix.su/team-board/docs
|
|
||||||
2. **Manual deploy** на сервере
|
|
||||||
3. **Restart services:** systemctl restart team-board-*
|
|
||||||
4. **Health check:** /health endpoints *(запланировано)*
|
|
||||||
|
|
||||||
**CI/CD:** Пока ручной деплой. В будущем — Gitea Actions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Ключевые решения
|
|
||||||
|
|
||||||
### Архитектурные решения из brainstorm-ов
|
|
||||||
|
|
||||||
#### Unified Message Model
|
|
||||||
**Решение:** Одна модель `Message` для чат-сообщений И комментариев к задачам.
|
|
||||||
**Обоснование:** Проще API, единый компонент UI, треды работают везде.
|
|
||||||
**Статус:** Принято в BRAINSTORM-UI-2026-02-22.
|
|
||||||
|
|
||||||
#### Tracker-Centric Architecture
|
|
||||||
**Решение:** Tracker — центральный хаб, всё остальное — клиенты.
|
|
||||||
**Обоснование:** Простота, масштабируемость, единая точка правды.
|
|
||||||
**Статус:** Принято в ARCHITECTURE.md.
|
|
||||||
|
|
||||||
#### WebSocket для событий, REST для мутаций
|
|
||||||
**Решение:** Разделение протоколов — WS только для push-событий.
|
|
||||||
**Обоснование:** Агенты через MCP tools, WebSocket не перегружен.
|
|
||||||
**Статус:** Принято в BRAINSTORM-PROTOCOL-2026-02-20.
|
|
||||||
|
|
||||||
#### Раздельные Listen Modes
|
|
||||||
**Решение:** `chat_listen` и `task_listen` — независимо.
|
|
||||||
**Обоснование:** Гибкая настройка подписок для разных ролей агентов.
|
|
||||||
**Статус:** Принято в BRAINSTORM-WS-V2-2026-02-22.
|
|
||||||
|
|
||||||
#### Подзадачи vs Этапы (Steps)
|
|
||||||
**Решение:** Подзадачи = задачи на канбане, этапы = чеклист внутри.
|
|
||||||
**Обоснование:** Не засорять канбан, но показывать прогресс агента.
|
|
||||||
**Статус:** Принято в BRAINSTORM-PROJECTS-2026-02-20.
|
|
||||||
|
|
||||||
#### Checkpoint Pattern для агентов
|
|
||||||
**Решение:** Короткие задачи + проверка событий между шагами.
|
|
||||||
**Обоснование:** LLM блокирующий, нужно решать "глухой агент".
|
|
||||||
**Статус:** Принято в BRAINSTORM-AGENTS-2026-02-20.
|
|
||||||
|
|
||||||
#### Агенты могут отклонять задачи
|
|
||||||
**Решение:** `reject_task` с обоснованием.
|
|
||||||
**Обоснование:** Агент не безмолвный исполнитель, может не согласиться.
|
|
||||||
**Статус:** Принято в BRAINSTORM-TASKS-2026-02-20.
|
|
||||||
|
|
||||||
#### Multi-repo проекты
|
|
||||||
**Решение:** `repo_urls: string[]` — массив репозиториев.
|
|
||||||
**Обоснование:** Современные проекты = frontend + backend + docs.
|
|
||||||
**Статус:** Принято в BRAINSTORM-PROJECTS-2026-02-20.
|
|
||||||
|
|
||||||
#### Файлы как Attachments
|
|
||||||
**Решение:** Отдельного файлового сервиса нет, файлы = вложения к сообщениям.
|
|
||||||
**Обоснование:** Простота, единая модель для чата и задач.
|
|
||||||
**Статус:** Принято в разговоре 2026-02-22, противоречило ранним документам.
|
|
||||||
|
|
||||||
### Отклонённые решения
|
|
||||||
|
|
||||||
#### Redis Streams для Event Bus
|
|
||||||
**Решение:** НЕ реализовывать сейчас.
|
|
||||||
**Обоснование:** В BRAINSTORM-MICROSERVICES было принято, но хозяин сомневается. Пока WS достаточно.
|
|
||||||
**Статус:** Отклонено, оставлено как "идея на будущее".
|
|
||||||
|
|
||||||
#### HTTP Callback для агентов
|
|
||||||
**Решение:** Только WebSocket для агентов.
|
|
||||||
**Обоснование:** Усложняет архитектуру, нет реальной потребности.
|
|
||||||
**Статус:** Отклонено из WS-PROTOCOL.md.
|
|
||||||
|
|
||||||
#### Threads в комментариях задач
|
|
||||||
**Решение:** Threads только в чатах, в задачах — простая лента.
|
|
||||||
**Обоснование:** В задачах контекст и так понятен, треды избыточны.
|
|
||||||
**Статус:** Принято в BRAINSTORM-UI-2026-02-22.
|
|
||||||
|
|
||||||
#### Глобальные vs Per-project лейблы
|
|
||||||
**Статус:** Остается открытым вопросом.
|
|
||||||
|
|
||||||
#### Реакции в чатах
|
|
||||||
**Статус:** Остается открытым вопросом (токен burn vs UX).
|
|
||||||
|
|
||||||
### На будущее (запланированы)
|
|
||||||
|
|
||||||
#### Web MCP Server
|
|
||||||
**Описание:** Отдельный MCP Server для работы любого LLM-клиента с трекером.
|
|
||||||
**Статус:** Идея из BRAINSTORM-PROTOCOL-2026-02-20.
|
|
||||||
|
|
||||||
#### Session Compaction
|
|
||||||
**Описание:** Сжатие длинной истории сессии агента в summary.
|
|
||||||
**Статус:** Детали в AGENT-CONNECTION-RESEARCH.md.
|
|
||||||
|
|
||||||
#### Telegram Bridge
|
|
||||||
**Описание:** Бот дублирует сообщения project chats в Telegram с топиками.
|
|
||||||
**Статус:** MVP описан в BRAINSTORM-MISC-2026-02-21.
|
|
||||||
|
|
||||||
#### OAuth/Authentik интеграция
|
|
||||||
**Описание:** Мультипользовательский режим с SSO.
|
|
||||||
**Статус:** Упоминается в архитектуре.
|
|
||||||
|
|
||||||
#### Agent Home с self-improving промптами
|
|
||||||
**Описание:** Агент сам улучшает свой промпт на основе опыта.
|
|
||||||
**Статус:** Детали в BRAINSTORM-PROMPTS-2026-02-21.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Нереализованные части
|
|
||||||
|
|
||||||
### Backend (запланировано)
|
|
||||||
|
|
||||||
- ❌ **Task events через WebSocket** — task.created/updated/assigned
|
|
||||||
- ❌ **Agent token auth для REST API** (только WS имеет проверку токенов)
|
|
||||||
- ❌ **Attachments API** — загрузка/скачивание файлов
|
|
||||||
- ❌ **Task dependencies** — валидация и уведомления о разблокировке
|
|
||||||
- ❌ **Watchers на задачах** — подписка на уведомления
|
|
||||||
- ❌ **Task watchers API** — POST/DELETE /tasks/{id}/watch
|
|
||||||
|
|
||||||
### Frontend (запланировано)
|
|
||||||
|
|
||||||
- ❌ **Task comments в TaskModal** — комментарии к задачам
|
|
||||||
- ❌ **Task steps в TaskModal** — live progress агентов
|
|
||||||
- ❌ **Agent management страница** — /agents с созданием и настройками
|
|
||||||
- ❌ **Project dashboard** — метрики и активность
|
|
||||||
- ❌ **Project settings** — настройки проекта
|
|
||||||
- ❌ **Mobile UI оптимизация** — полная адаптация под телефоны
|
|
||||||
|
|
||||||
### Infrastructure (запланировано)
|
|
||||||
|
|
||||||
- ❌ **Health check endpoints** — /health для мониторинга
|
|
||||||
- ❌ **Database migrations** — Alembic вместо recreate
|
|
||||||
- ❌ **CI/CD pipeline** — Gitea Actions автодеплой
|
|
||||||
- ❌ **Monitoring/metrics** — Grafana dashboard
|
|
||||||
- ❌ **Backup strategy** — автоматические бэкапы БД
|
|
||||||
|
|
||||||
### Agent System (запланировано)
|
|
||||||
|
|
||||||
- ❌ **MCP Tools полный набор** — для picogent агентов
|
|
||||||
- ❌ **Session resume** — восстановление контекста при переподключении
|
|
||||||
- ❌ **Agent home directory** — workspace, memory, sessions
|
|
||||||
- ❌ **Multi-instance агентов** — scaling одной роли
|
|
||||||
- ❌ **Self-improving промпты** — agent.prompt.md обновляется агентом
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
Этот документ v2.0 создан на основе полного анализа всех 24 файлов документации Team Board по состоянию на 2026-02-23. Он объединяет все принятые решения из brainstorm-ов, архитектурные концепции и текущее состояние реализации в единую точку правды.
|
|
||||||
|
|
||||||
**Принцип документа:** Описываем решения и концепции, а не текущее состояние кода. Если что-то запланировано но не реализовано — помечено как "Запланировано", а не TODO.
|
|
||||||
|
|
||||||
При расхождении между документацией и кодом — **приоритет у кода** (он правдивее), документ нужно обновить.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,585 +0,0 @@
|
|||||||
# MCP Tools Architecture — Team Board
|
|
||||||
|
|
||||||
**Версия:** 1.0
|
|
||||||
**Дата:** 2026-02-23
|
|
||||||
**Автор:** Winston (BMAD)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Исследование MCP
|
|
||||||
|
|
||||||
### Что такое MCP
|
|
||||||
|
|
||||||
Model Context Protocol (MCP) — открытый стандарт для подключения AI приложений к внешним системам. MCP работает как "USB-C для AI" — стандартизированный способ подключения ИИ к данным, инструментам и рабочим процессам.
|
|
||||||
|
|
||||||
**Архитектура клиент-сервер:**
|
|
||||||
- **MCP Host** (AI приложение): Управляет одним или множеством MCP клиентов
|
|
||||||
- **MCP Client**: Поддерживает соединение с MCP сервером и получает контекст
|
|
||||||
- **MCP Server**: Программа, предоставляющая контекст MCP клиентам
|
|
||||||
|
|
||||||
**Двухуровневая архитектура:**
|
|
||||||
|
|
||||||
1. **Data layer** (Протокол данных):
|
|
||||||
- JSON-RPC 2.0 для коммуникации клиент-сервер
|
|
||||||
- Управление жизненным циклом, capabilities negotiation
|
|
||||||
- Основные примитивы: Tools, Resources, Prompts
|
|
||||||
|
|
||||||
2. **Transport layer** (Транспорт):
|
|
||||||
- **STDIO**: Прямое соединение между процессами (локальные серверы)
|
|
||||||
- **HTTP Streamable**: HTTP POST с Server-Sent Events (удаленные серверы)
|
|
||||||
|
|
||||||
### Ключевые концепции MCP
|
|
||||||
|
|
||||||
**Серверные примитивы:**
|
|
||||||
- **Tools**: Выполняемые функции (например, API вызовы, операции с файлами)
|
|
||||||
- **Resources**: Источники данных для контекста (содержимое файлов, записи БД)
|
|
||||||
- **Prompts**: Шаблоны для взаимодействия с языковыми моделями
|
|
||||||
|
|
||||||
**Клиентские примитивы:**
|
|
||||||
- **Sampling**: Запрос language model completion от клиента
|
|
||||||
- **Elicitation**: Запрос дополнительной информации от пользователя
|
|
||||||
- **Logging**: Отправка логов для отладки
|
|
||||||
|
|
||||||
**Lifecycle Management:**
|
|
||||||
1. Инициализация соединения
|
|
||||||
2. Negotiation capabilities через handshake
|
|
||||||
3. Динамическое обнаружение tools через `tools/list`
|
|
||||||
4. Выполнение tools через `tools/call`
|
|
||||||
5. Real-time уведомления о изменениях
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Анализ текущей архитектуры Picogent
|
|
||||||
|
|
||||||
### Как работает сейчас
|
|
||||||
|
|
||||||
Picogent — агент для Team Board с архитектурой:
|
|
||||||
|
|
||||||
```
|
|
||||||
Picogent Agent
|
|
||||||
├── WebSocket Transport (WsClientTransport)
|
|
||||||
├── REST Client (TrackerClient)
|
|
||||||
├── Event Router (EventRouter)
|
|
||||||
└── Claude API Integration
|
|
||||||
```
|
|
||||||
|
|
||||||
**Текущий стек:**
|
|
||||||
- **Transport**: WebSocket для событий + REST для мутаций
|
|
||||||
- **Events**: `task.assigned`, `message.new` → обработка через EventRouter
|
|
||||||
- **Actions**: Только REST API через TrackerClient
|
|
||||||
- **LLM**: Прямая интеграция с Claude API (Anthropic SDK)
|
|
||||||
|
|
||||||
### Интеграционные точки
|
|
||||||
|
|
||||||
1. **Agent.json конфигурация:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"capabilities": ["coding", "review"],
|
|
||||||
"chat_listen": "mentions",
|
|
||||||
"task_listen": "assigned"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **EventRouter обрабатывает:**
|
|
||||||
- `task.assigned` → запускает `runAgent()`
|
|
||||||
- `message.new` → отвечает если упомянут
|
|
||||||
|
|
||||||
3. **TrackerClient предоставляет REST методы:**
|
|
||||||
- Tasks: `listTasks()`, `takeTask()`, `updateTask()`
|
|
||||||
- Messages: `sendMessage()`, `listMessages()`
|
|
||||||
- Projects: `listProjects()`, `getProject()`
|
|
||||||
|
|
||||||
### Ограничения текущего подхода
|
|
||||||
|
|
||||||
- **Монолитный EventRouter**: Вся логика в одном классе
|
|
||||||
- **Hardcoded инструменты**: REST методы зашиты в код агента
|
|
||||||
- **Нет tool discovery**: ИИ не знает какие действия доступны
|
|
||||||
- **Сложное расширение**: Добавление новых действий требует изменения кода
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Решение: MCP vs Function Calling
|
|
||||||
|
|
||||||
### Сравнение подходов
|
|
||||||
|
|
||||||
| Критерий | MCP Server | Function Calling | Рекомендация |
|
|
||||||
|----------|-----------|------------------|-------------|
|
|
||||||
| **Стандартизация** | ✅ Открытый стандарт | ❌ Зависит от LLM провайдера | MCP |
|
|
||||||
| **Совместимость** | ✅ Любые MCP hosts | ❌ Только Claude/OpenAI | MCP |
|
|
||||||
| **Tool Discovery** | ✅ Динамический `tools/list` | ❌ Статичные definitions | MCP |
|
|
||||||
| **Real-time updates** | ✅ Notifications | ❌ Нет | MCP |
|
|
||||||
| **Сложность реализации** | ⚠️ Средняя (протокол) | ✅ Простая (JSON schema) | Function Calling |
|
|
||||||
| **Готовые SDK** | ✅ Python, TypeScript | ❌ Самописные | MCP |
|
|
||||||
|
|
||||||
### Гибридный подход — рекомендация
|
|
||||||
|
|
||||||
**Используем Function Calling с MCP-совместимой архитектурой:**
|
|
||||||
|
|
||||||
1. **Tool definitions** в формате совместимом с MCP
|
|
||||||
2. **Простая реализация** без полного MCP сервера
|
|
||||||
3. **Готовность к миграции** на полный MCP в будущем
|
|
||||||
|
|
||||||
**Обоснование:**
|
|
||||||
- **Прагматизм**: Function calling проще реализовать и отладить
|
|
||||||
- **Быстрый результат**: Не нужен отдельный MCP сервер
|
|
||||||
- **Совместимость**: Структуры данных готовы для MCP
|
|
||||||
- **Эволюция**: Легко мигрировать на полный MCP позже
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Каталог инструментов
|
|
||||||
|
|
||||||
### 4.1 Tasks (Задачи)
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `list_tasks` | Список задач с фильтрацией | `project_id?`, `status?`, `assignee?`, `labels?` | `TaskOut[]` |
|
|
||||||
| `get_task` | Получить задачу по ID | `task_id: string` | `TaskOut` |
|
|
||||||
| `create_task` | Создать новую задачу | `project_slug: string`, `title: string`, `description?`, `type?`, `priority?`, `assignee_slug?` | `TaskOut` |
|
|
||||||
| `update_task` | Обновить поля задачи | `task_id: string`, `title?`, `status?`, `priority?`, `assignee_slug?` | `TaskOut` |
|
|
||||||
| `take_task` | Взять задачу себе (атомарно) | `task_id: string`, `agent_slug: string` | `TaskOut` |
|
|
||||||
| `reject_task` | Отклонить задачу с причиной | `task_id: string`, `reason: string` | `{ok: true, reason: string}` |
|
|
||||||
| `assign_task` | Назначить задачу другому | `task_id: string`, `assignee_slug: string` | `TaskOut` |
|
|
||||||
| `watch_task` | Подписаться на уведомления | `task_id: string`, `agent_slug: string` | `{ok: true, watchers: string[]}` |
|
|
||||||
|
|
||||||
**Пример tool definition:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "take_task",
|
|
||||||
"title": "Взять задачу в работу",
|
|
||||||
"description": "Атомарно назначить задачу агенту, если она свободна",
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"task_id": {"type": "string", "description": "ID или ключ задачи (TE-15)"},
|
|
||||||
"agent_slug": {"type": "string", "description": "Slug агента"}
|
|
||||||
},
|
|
||||||
"required": ["task_id", "agent_slug"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Steps (Этапы задач)
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `list_steps` | Этапы задачи | `task_id: string` | `StepOut[]` |
|
|
||||||
| `add_step` | Добавить этап в чеклист | `task_id: string`, `title: string` | `StepOut` |
|
|
||||||
| `complete_step` | Отметить этап выполненным | `task_id: string`, `step_id: string` | `StepOut` |
|
|
||||||
| `update_step` | Обновить название этапа | `task_id: string`, `step_id: string`, `title?: string`, `done?: boolean` | `StepOut` |
|
|
||||||
|
|
||||||
### 4.3 Messages (Сообщения и комментарии)
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `send_message` | Отправить сообщение в чат или комментарий к задаче | `content: string`, `chat_id?: string`, `task_id?: string`, `mentions?: string[]` | `MessageOut` |
|
|
||||||
| `list_messages` | Получить сообщения | `chat_id?: string`, `task_id?: string`, `limit?: number` | `MessageOut[]` |
|
|
||||||
| `reply_message` | Ответить в треде | `parent_id: string`, `content: string` | `MessageOut` |
|
|
||||||
|
|
||||||
**Unified Message модель** — одна сущность для:
|
|
||||||
- **Chat messages**: `chat_id` заполнен
|
|
||||||
- **Task comments**: `task_id` заполнен
|
|
||||||
- **Thread replies**: `parent_id` заполнен
|
|
||||||
|
|
||||||
### 4.4 Projects (Проекты)
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `list_projects` | Список активных проектов | - | `ProjectOut[]` |
|
|
||||||
| `get_project` | Информация о проекте | `slug: string` | `ProjectOut` |
|
|
||||||
|
|
||||||
### 4.5 Members (Участники)
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `list_members` | Участники проекта | - | `MemberOut[]` |
|
|
||||||
| `get_member` | Информация об участнике | `slug: string` | `MemberOut` |
|
|
||||||
| `update_my_status` | Обновить свой статус | `status: string` | `{status: string}` |
|
|
||||||
|
|
||||||
### 4.6 Files (Файлы) — будущие
|
|
||||||
|
|
||||||
| Tool Name | Description | Parameters | Returns |
|
|
||||||
|-----------|-------------|------------|---------|
|
|
||||||
| `upload_file` | Загрузить файл к сообщению | `message_id: string`, `filename: string`, `content: base64` | `AttachmentOut` |
|
|
||||||
| `list_files` | Файлы задачи/проекта | `task_id?: string`, `project_id?: string` | `AttachmentOut[]` |
|
|
||||||
| `download_file` | Скачать файл | `attachment_id: string` | `{filename: string, content: base64}` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Архитектура реализации
|
|
||||||
|
|
||||||
### 5.1 Компонентная структура
|
|
||||||
|
|
||||||
```
|
|
||||||
picogent/
|
|
||||||
├── src/
|
|
||||||
│ ├── tools/ # MCP-совместимые tools
|
|
||||||
│ │ ├── registry.ts # Реестр всех tools
|
|
||||||
│ │ ├── tasks.ts # Task tools
|
|
||||||
│ │ ├── messages.ts # Message tools
|
|
||||||
│ │ ├── projects.ts # Project tools
|
|
||||||
│ │ └── members.ts # Member tools
|
|
||||||
│ ├── mcp/
|
|
||||||
│ │ ├── types.ts # MCP-совместимые типы
|
|
||||||
│ │ ├── tool-executor.ts # Выполнение tools
|
|
||||||
│ │ └── tool-builder.ts # Построение tool definitions
|
|
||||||
│ ├── agent.ts # Интеграция с Claude + tools
|
|
||||||
│ └── router.ts # Обновлённый EventRouter
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Tool Registry (Центральный реестр)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// tools/registry.ts
|
|
||||||
export interface ToolDefinition {
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
inputSchema: JSONSchema;
|
|
||||||
handler: (params: any, context: ToolContext) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolContext {
|
|
||||||
trackerClient: TrackerClient;
|
|
||||||
agentSlug: string;
|
|
||||||
transport: WsClientTransport;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ToolRegistry {
|
|
||||||
private tools = new Map<string, ToolDefinition>();
|
|
||||||
|
|
||||||
register(tool: ToolDefinition): void {
|
|
||||||
this.tools.set(tool.name, tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
list(): ToolDefinition[] {
|
|
||||||
return Array.from(this.tools.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(name: string, params: any, context: ToolContext): Promise<any> {
|
|
||||||
const tool = this.tools.get(name);
|
|
||||||
if (!tool) throw new Error(`Tool not found: ${name}`);
|
|
||||||
return tool.handler(params, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Task Tools Implementation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// tools/tasks.ts
|
|
||||||
import { ToolDefinition } from './registry.js';
|
|
||||||
|
|
||||||
export const takeTaskTool: ToolDefinition = {
|
|
||||||
name: 'take_task',
|
|
||||||
title: 'Взять задачу в работу',
|
|
||||||
description: 'Атомарно назначить задачу агенту, если она свободна',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
task_id: { type: 'string', description: 'ID задачи' },
|
|
||||||
agent_slug: { type: 'string', description: 'Slug агента' }
|
|
||||||
},
|
|
||||||
required: ['task_id', 'agent_slug']
|
|
||||||
},
|
|
||||||
async handler(params, context) {
|
|
||||||
try {
|
|
||||||
const result = await context.trackerClient.takeTask(
|
|
||||||
params.task_id,
|
|
||||||
params.agent_slug
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: `✅ Задача ${result.key} взята в работу`
|
|
||||||
}],
|
|
||||||
structuredContent: result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: `❌ Ошибка: ${error.message}`
|
|
||||||
}],
|
|
||||||
isError: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listTasksTool: ToolDefinition = {
|
|
||||||
name: 'list_tasks',
|
|
||||||
title: 'Список задач',
|
|
||||||
description: 'Получить список задач с фильтрацией',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
project_id: { type: 'string', description: 'ID проекта' },
|
|
||||||
status: { type: 'string', enum: ['backlog', 'todo', 'in_progress', 'in_review', 'done'] },
|
|
||||||
assignee: { type: 'string', description: 'Slug исполнителя' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async handler(params, context) {
|
|
||||||
const tasks = await context.trackerClient.listTasks(params);
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: `Найдено задач: ${tasks.length}\n${tasks.map(t => `- ${t.key}: ${t.title} (${t.status})`).join('\n')}`
|
|
||||||
}],
|
|
||||||
structuredContent: tasks
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 Интеграция с Claude
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// agent.ts (модифицированный)
|
|
||||||
import { ToolRegistry } from './tools/registry.js';
|
|
||||||
|
|
||||||
export async function* runAgent(prompt: string, options: AgentOptions): AsyncIterator<AgentMessage> {
|
|
||||||
const toolRegistry = new ToolRegistry();
|
|
||||||
|
|
||||||
// Регистрируем все tools
|
|
||||||
registerAllTools(toolRegistry);
|
|
||||||
|
|
||||||
// Создаём tool definitions для Claude
|
|
||||||
const tools = toolRegistry.list().map(tool => ({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
input_schema: tool.inputSchema
|
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await anthropic.messages.create({
|
|
||||||
model: options.model,
|
|
||||||
messages: [{ role: 'user', content: prompt }],
|
|
||||||
tools,
|
|
||||||
system: options.systemPrompt
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обрабатываем tool calls
|
|
||||||
for (const content of response.content) {
|
|
||||||
if (content.type === 'tool_use') {
|
|
||||||
const result = await toolRegistry.execute(
|
|
||||||
content.name,
|
|
||||||
content.input,
|
|
||||||
{ trackerClient: options.trackerClient, agentSlug: options.agentSlug }
|
|
||||||
);
|
|
||||||
|
|
||||||
yield {
|
|
||||||
type: 'text',
|
|
||||||
content: result.content?.[0]?.text || 'Tool executed',
|
|
||||||
sessionId: options.sessionId
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
yield {
|
|
||||||
type: 'text',
|
|
||||||
content: content.text,
|
|
||||||
sessionId: options.sessionId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.5 Обновлённый EventRouter
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// router.ts (упрощённый)
|
|
||||||
export class EventRouter {
|
|
||||||
constructor(
|
|
||||||
private config: AgentConfig,
|
|
||||||
private toolRegistry: ToolRegistry,
|
|
||||||
private taskTracker: TaskTracker,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async handleEvent(event: TrackerEvent): Promise<void> {
|
|
||||||
const context = {
|
|
||||||
trackerClient: new TrackerClient(this.config.trackerUrl, this.config.token),
|
|
||||||
agentSlug: this.config.slug,
|
|
||||||
transport: this.taskTracker
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (event.event) {
|
|
||||||
case 'task.assigned':
|
|
||||||
await this.handleTaskAssigned(event.data, context);
|
|
||||||
break;
|
|
||||||
case 'message.new':
|
|
||||||
await this.handleMessageNew(event.data, context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleTaskAssigned(data: any, context: ToolContext): Promise<void> {
|
|
||||||
const task = data.task || data;
|
|
||||||
|
|
||||||
// Строим промпт с доступными tools
|
|
||||||
const toolsDescription = this.toolRegistry.list()
|
|
||||||
.map(t => `- ${t.name}: ${t.description}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const prompt = `
|
|
||||||
# Задача: ${task.key} — ${task.title}
|
|
||||||
|
|
||||||
${task.description || ''}
|
|
||||||
|
|
||||||
## Доступные инструменты:
|
|
||||||
${toolsDescription}
|
|
||||||
|
|
||||||
Выполни задачу используя доступные инструменты.
|
|
||||||
Начни с обновления статуса на "in_progress".
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Запускаем агента с tools
|
|
||||||
for await (const msg of runAgent(prompt, {
|
|
||||||
toolRegistry: this.toolRegistry,
|
|
||||||
agentSlug: this.config.slug,
|
|
||||||
trackerClient: context.trackerClient
|
|
||||||
})) {
|
|
||||||
// Логируем результат
|
|
||||||
console.log(`Agent output: ${msg.content}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.6 Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
picogent/
|
|
||||||
├── src/
|
|
||||||
│ ├── tools/
|
|
||||||
│ │ ├── registry.ts # Центральный реестр tools
|
|
||||||
│ │ ├── tasks.ts # 8 task tools
|
|
||||||
│ │ ├── steps.ts # 4 step tools
|
|
||||||
│ │ ├── messages.ts # 3 message tools
|
|
||||||
│ │ ├── projects.ts # 2 project tools
|
|
||||||
│ │ ├── members.ts # 3 member tools
|
|
||||||
│ │ └── index.ts # Export всех tools
|
|
||||||
│ ├── mcp/
|
|
||||||
│ │ ├── types.ts # MCP-совместимые типы
|
|
||||||
│ │ └── executor.ts # Tool execution engine
|
|
||||||
│ ├── tracker/
|
|
||||||
│ │ ├── client.ts # REST client (существующий)
|
|
||||||
│ │ └── types.ts # Tracker API types
|
|
||||||
│ ├── transport/
|
|
||||||
│ │ └── ws-client.ts # WebSocket transport (существующий)
|
|
||||||
│ ├── agent.ts # Модифицированный для tools
|
|
||||||
│ ├── router.ts # Упрощённый EventRouter
|
|
||||||
│ └── index.ts # Точка входа
|
|
||||||
├── package.json # + @anthropic-ai/sdk dependency
|
|
||||||
└── agent.json # Конфигурация (существующая)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. План реализации
|
|
||||||
|
|
||||||
### Phase 1: Базовая инфраструктура tools (Неделя 1)
|
|
||||||
|
|
||||||
**День 1-2: Создание Tool Registry**
|
|
||||||
- [ ] `src/tools/registry.ts` — интерфейсы и реестр
|
|
||||||
- [ ] `src/mcp/types.ts` — MCP-совместимые типы
|
|
||||||
- [ ] Тесты для registry
|
|
||||||
|
|
||||||
**День 3-4: Интеграция с Claude**
|
|
||||||
- [ ] Модификация `agent.ts` для поддержки tools
|
|
||||||
- [ ] Tool execution pipeline
|
|
||||||
- [ ] Error handling для tool calls
|
|
||||||
|
|
||||||
**День 5-7: Базовые Task Tools**
|
|
||||||
- [ ] `take_task`, `list_tasks`, `get_task`
|
|
||||||
- [ ] Интеграция с существующим TrackerClient
|
|
||||||
- [ ] Тестирование на простых задачах
|
|
||||||
|
|
||||||
### Phase 2: Полный набор tools (Неделя 2)
|
|
||||||
|
|
||||||
**День 1-3: Все Task и Step Tools**
|
|
||||||
- [ ] Реализация всех 8 task tools
|
|
||||||
- [ ] 4 step tools для чеклистов
|
|
||||||
- [ ] Валидация параметров и ошибок
|
|
||||||
|
|
||||||
**День 4-5: Message Tools**
|
|
||||||
- [ ] `send_message`, `list_messages`, `reply_message`
|
|
||||||
- [ ] Поддержка unified модели (чат + комментарии)
|
|
||||||
|
|
||||||
**День 6-7: Project и Member Tools**
|
|
||||||
- [ ] Project tools: `list_projects`, `get_project`
|
|
||||||
- [ ] Member tools: `list_members`, `update_my_status`
|
|
||||||
- [ ] Интеграционные тесты
|
|
||||||
|
|
||||||
### Phase 3: Продвинутые возможности (Неделя 3)
|
|
||||||
|
|
||||||
**День 1-2: Обновлённый EventRouter**
|
|
||||||
- [ ] Рефакторинг router.ts под tools
|
|
||||||
- [ ] Улучшенная обработка `task.assigned`
|
|
||||||
- [ ] Dynamic tool discovery
|
|
||||||
|
|
||||||
**День 3-4: Error Handling и Reliability**
|
|
||||||
- [ ] Robust error handling в tools
|
|
||||||
- [ ] Retry logic для API calls
|
|
||||||
- [ ] Graceful degradation
|
|
||||||
|
|
||||||
**День 5-7: Документация и тесты**
|
|
||||||
- [ ] Tool documentation
|
|
||||||
- [ ] Integration tests
|
|
||||||
- [ ] Performance тесты
|
|
||||||
|
|
||||||
### Phase 4: Готовность к MCP миграции (Неделя 4)
|
|
||||||
|
|
||||||
**День 1-3: MCP Compatibility Layer**
|
|
||||||
- [ ] Full MCP tool definitions format
|
|
||||||
- [ ] MCP server skeleton (не активный)
|
|
||||||
- [ ] Migration path documentation
|
|
||||||
|
|
||||||
**День 4-5: Production Deploy**
|
|
||||||
- [ ] Deploy обновлённого Picogent
|
|
||||||
- [ ] Monitoring и логирование
|
|
||||||
- [ ] Performance optimization
|
|
||||||
|
|
||||||
**День 6-7: Итерации на основе feedback**
|
|
||||||
- [ ] Bug fixes
|
|
||||||
- [ ] UX improvements
|
|
||||||
- [ ] Documentation updates
|
|
||||||
|
|
||||||
### Критерии готовности
|
|
||||||
|
|
||||||
**Phase 1 Complete:**
|
|
||||||
- ✅ Агент может взять задачу используя `take_task` tool
|
|
||||||
- ✅ Список задач через `list_tasks` tool работает
|
|
||||||
- ✅ Error handling для недоступных tools
|
|
||||||
|
|
||||||
**Phase 2 Complete:**
|
|
||||||
- ✅ Все 20 tools реализованы и работают
|
|
||||||
- ✅ Агент может выполнить полный цикл: взять задачу → добавить этапы → обновить статус → прокомментировать
|
|
||||||
|
|
||||||
**Phase 3 Complete:**
|
|
||||||
- ✅ Production-ready качество кода
|
|
||||||
- ✅ Comprehensive error handling
|
|
||||||
- ✅ Full integration test suite
|
|
||||||
|
|
||||||
**Phase 4 Complete:**
|
|
||||||
- ✅ MCP-compatible архитектура
|
|
||||||
- ✅ Готовность к миграции на полный MCP сервер
|
|
||||||
- ✅ Документация для developers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
**Выбранный подход** — гибридная архитектура с Function Calling в MCP-совместимом формате — даёт нам:
|
|
||||||
|
|
||||||
1. **Быстрый результат**: Можно реализовать за 3-4 недели
|
|
||||||
2. **Простота**: Не требует отдельного MCP сервера
|
|
||||||
3. **Гибкость**: 20+ tools покрывают все операции Tracker API
|
|
||||||
4. **Масштабируемость**: Tool Registry легко расширяется
|
|
||||||
5. **Будущее**: Готовность к миграции на полный MCP
|
|
||||||
|
|
||||||
**Ключевые преимущества:**
|
|
||||||
- **Dynamic tool discovery**: ИИ знает какие действия доступны
|
|
||||||
- **Structured error handling**: Понятные ошибки для self-correction
|
|
||||||
- **Unified architecture**: Все операции через единый интерфейс
|
|
||||||
- **MCP compatibility**: Готовность к стандарту будущего
|
|
||||||
|
|
||||||
Team Board получает полнофункциональных агентов с богатым набором инструментов для автономной работы с задачами, проектами и командой.
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
# Обзор архитектуры OpenClaw — что можно позаимствовать
|
|
||||||
|
|
||||||
## 1. Agent Loop (Agentic Loop)
|
|
||||||
|
|
||||||
**Как работает в OpenClaw:**
|
|
||||||
- Gateway получает сообщение → резолвит сессию → запускает `runEmbeddedPiAgent`
|
|
||||||
- Runs сериализуются per-session (queue) — нет race conditions
|
|
||||||
- Pi Agent Core внутри: `session.prompt()` → LLM → tool calls → results → LLM → ...
|
|
||||||
- События стримятся: `text_delta`, `tool_execution_start/end`, `message_end`, `agent_end`
|
|
||||||
- Timeout enforcement — если агент завис, run абортится
|
|
||||||
|
|
||||||
**Что забрать:**
|
|
||||||
- ✅ **Run queue per session** — сериализация запросов, нет параллельных runs в одной сессии
|
|
||||||
- ✅ **Timeout на run** — агент не может зависнуть навечно
|
|
||||||
- ✅ **Event streaming** — стримить text deltas, tool calls, reasoning в реальном времени
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Память (Memory)
|
|
||||||
|
|
||||||
**Как работает в OpenClaw:**
|
|
||||||
- Два слоя: `memory/YYYY-MM-DD.md` (daily, append-only) + `MEMORY.md` (curated long-term)
|
|
||||||
- **Memory flush перед компакцией** — silent agentic turn: "запиши важное прежде чем контекст сожмётся"
|
|
||||||
- **Semantic search** (memory_search) — hybrid BM25 + vector, по чанкам ~400 токенов
|
|
||||||
- **memory_get** — точечное чтение файла по path + line range
|
|
||||||
- Агент **сам пишет** в файлы через read/write/edit tools
|
|
||||||
|
|
||||||
**Что забрать:**
|
|
||||||
- ✅ **Memory flush перед компакцией** — критично для long-running агентов
|
|
||||||
- ✅ **Semantic search** — когда памяти станет много (>50K chars), нужен поиск
|
|
||||||
- ✅ **Двухуровневая память** — уже решили (agent.md + projects/{uuid}/)
|
|
||||||
- 🔜 **Vector search** — отложить до масштаба
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Контекстное окно и компакция
|
|
||||||
|
|
||||||
**Как работает в OpenClaw:**
|
|
||||||
- Каждая модель имеет `contextWindow` (макс токенов)
|
|
||||||
- **Auto-compaction**: когда сессия близка к лимиту → суммаризация старых сообщений → compact entry
|
|
||||||
- **Session pruning**: обрезка старых tool results (in-memory, не трогает JSONL)
|
|
||||||
- **Cache-TTL pruning** (Anthropic): если кэш протух — обрезать перед re-cache
|
|
||||||
- `reserveTokensFloor` — всегда оставлять N токенов для ответа
|
|
||||||
|
|
||||||
**Компакция:**
|
|
||||||
1. Определить что session близка к лимиту (token estimate)
|
|
||||||
2. Запустить memory flush (silent turn — "запиши важное")
|
|
||||||
3. Суммаризировать старые сообщения через LLM → compact summary
|
|
||||||
4. Заменить старые сообщения на summary в истории
|
|
||||||
5. Персистить в JSONL
|
|
||||||
|
|
||||||
**Что забрать:**
|
|
||||||
- ✅ **Auto-compaction** — обязательно для long-running агентов
|
|
||||||
- ✅ **Token estimation** — считать примерный размер контекста перед каждым вызовом
|
|
||||||
- ✅ **Reserve tokens** — не заполнять окно до упора
|
|
||||||
- ✅ **Memory flush trigger** — перед компакцией напоминать агенту записать важное
|
|
||||||
- 🔜 **Session pruning** — обрезка tool results (для Anthropic cache optimization)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Streaming и доставка ответов
|
|
||||||
|
|
||||||
**Как работает в OpenClaw:**
|
|
||||||
- **Block streaming** — отправка завершённых блоков текста по мере генерации
|
|
||||||
- **Token streaming** (Telegram) — обновление draft-bubble частичным текстом
|
|
||||||
- **Reasoning streaming** — отдельный поток для thinking/reasoning блоков
|
|
||||||
- Chunking: min/max chars, break preference (text_end vs message_end)
|
|
||||||
|
|
||||||
**Что забрать для Team Board:**
|
|
||||||
- ✅ **Streaming через WS** — агент стримит text deltas через WebSocket
|
|
||||||
- ✅ **Reasoning blocks** — отдельный тип сообщения `thinking` в WS protocol
|
|
||||||
- ✅ **Block replies** — отправлять частичные ответы (не ждать завершения)
|
|
||||||
|
|
||||||
### Как реализовать в Team Board:
|
|
||||||
|
|
||||||
**Новые WS event types:**
|
|
||||||
```
|
|
||||||
agent.stream.start → агент начал генерацию
|
|
||||||
agent.stream.delta → { text: "частичный текст", type: "text"|"thinking" }
|
|
||||||
agent.stream.tool → { tool: "update_task", status: "running"|"done" }
|
|
||||||
agent.stream.end → генерация завершена, финальное сообщение
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- Показывать typing indicator + streaming text в ChatPanel
|
|
||||||
- Раскрывающийся блок `<details>` для thinking/reasoning
|
|
||||||
- Tool calls показывать как "🔧 Обновляет задачу..." inline
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Ответ агента через WS (не REST)
|
|
||||||
|
|
||||||
**Текущая проблема:** агент вызывает REST `POST /messages` чтобы ответить → broadcast через WS. Лишний round-trip.
|
|
||||||
|
|
||||||
**Как должно быть:**
|
|
||||||
1. Tracker получает message.new → отправляет агенту через WS
|
|
||||||
2. Агент генерирует ответ → стримит через WS (`agent.stream.delta`)
|
|
||||||
3. Tracker принимает `agent.reply` через WS → сохраняет в БД → broadcast всем
|
|
||||||
4. Или: агент отправляет `chat.send` через WS (уже поддерживается!)
|
|
||||||
|
|
||||||
**Минимальное изменение:**
|
|
||||||
- Router при auto-reply использует WS `chat.send` вместо REST POST
|
|
||||||
- Для explicit send_message tool — тоже через WS
|
|
||||||
- REST send_message остаётся для внешних клиентов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Что НЕ нужно забирать
|
|
||||||
|
|
||||||
- ❌ **Multi-channel routing** (WhatsApp, Telegram, Discord) — у нас один канал (WebSocket)
|
|
||||||
- ❌ **Node/device pairing** — не наш use case
|
|
||||||
- ❌ **Canvas/A2UI** — пока не нужно
|
|
||||||
- ❌ **OAuth proxy** — решается через litellm или провайдер SDK
|
|
||||||
- ❌ **Plugin system** — оверинжиниринг для наших масштабов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Roadmap заимствований
|
|
||||||
|
|
||||||
### Phase 1 (сейчас)
|
|
||||||
- [ ] Agent отвечает через WS вместо REST
|
|
||||||
- [ ] Streaming text deltas через WS events
|
|
||||||
- [ ] Reasoning/thinking block в UI (раскрывающийся)
|
|
||||||
|
|
||||||
### Phase 2 (скоро)
|
|
||||||
- [ ] Auto-compaction для длинных сессий
|
|
||||||
- [ ] Memory flush перед компакцией
|
|
||||||
- [ ] Token estimation (считать размер контекста)
|
|
||||||
- [ ] Run timeout (kill зависших агентов)
|
|
||||||
|
|
||||||
### Phase 3 (позже)
|
|
||||||
- [ ] Semantic memory search (vector + BM25)
|
|
||||||
- [ ] Session pruning (обрезка tool results)
|
|
||||||
- [ ] Block streaming (частичные ответы до завершения)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Thinking/Reasoning в UI
|
|
||||||
|
|
||||||
**Реализация раскрывающегося блока:**
|
|
||||||
|
|
||||||
**Backend (Tracker WS protocol):**
|
|
||||||
```json
|
|
||||||
{"type": "agent.stream.delta", "data": {"agent_id": "...", "block_type": "thinking", "text": "Думаю..."}}
|
|
||||||
{"type": "agent.stream.delta", "data": {"agent_id": "...", "block_type": "text", "text": "Ответ"}}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend (ChatPanel):**
|
|
||||||
```tsx
|
|
||||||
{message.thinking && (
|
|
||||||
<details className="text-gray-400 text-sm">
|
|
||||||
<summary>💭 Размышления агента</summary>
|
|
||||||
<pre>{message.thinking}</pre>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Хранение в БД:**
|
|
||||||
- Поле `thinking` в Message модели (nullable text)
|
|
||||||
- Или отдельная таблица reasoning_blocks
|
|
||||||
- Проще: JSON поле `metadata` с ключом `thinking`
|
|
||||||
|
|
||||||
**Что нужно от LLM:**
|
|
||||||
- Anthropic: `thinking` parameter в API request → возвращает `thinking` блоки
|
|
||||||
- OpenAI: `reasoning_effort` → скрытый reasoning (не доступен)
|
|
||||||
- Только Anthropic (Claude) реально отдаёт thinking content
|
|
||||||
360
PLANNING.md
360
PLANNING.md
@ -1,360 +0,0 @@
|
|||||||
# Planning — Team Board
|
|
||||||
Дата: 2026-02-23
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
- Каждый эпик = самодостаточный блок, можно релизить независимо
|
|
||||||
- Порядок = зависимости + приоритет из Architecture Review
|
|
||||||
- Никаких параллельных эпиков — строго последовательно
|
|
||||||
- Каждая story имеет чёткие критерии приёмки
|
|
||||||
|
|
||||||
## Эпик 1: WebSocket Foundation — Исправить критические баги
|
|
||||||
Цель: Устранить P0 блокеры из Architecture Review — WS collision и отсутствие task events
|
|
||||||
Зависимости: нет
|
|
||||||
|
|
||||||
### Story 1.1: Исправить WebSocket connection collision
|
|
||||||
**Как** системный разработчик **я хочу** чтобы множественные подключения одного пользователя не вытесняли друг друга **чтобы** система работала корректно при multiple tabs/reconnect
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `ConnectionManager.clients` изменён с `dict[str, ConnectedClient]` на `dict[str, list[ConnectedClient]]`
|
|
||||||
- [ ] Добавлен `sessions: dict[str, ConnectedClient]` с уникальными session_id
|
|
||||||
- [ ] При подключении создаётся уникальный session_id
|
|
||||||
- [ ] При отключении удаляется только конкретная сессия
|
|
||||||
- [ ] Broadcast отправляется всем сессиям пользователя
|
|
||||||
|
|
||||||
**Компоненты:** Tracker
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 1.2: Добавить broadcast task events
|
|
||||||
**Как** агент **я хочу** получать уведомления о создании/изменении задач **чтобы** реагировать на новую работу в проекте
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/tasks` вызывает `broadcast_task_event("task.created")`
|
|
||||||
- [ ] `PATCH /api/v1/tasks/{id}` вызывает `broadcast_task_event("task.updated")`
|
|
||||||
- [ ] `DELETE /api/v1/tasks/{id}` вызывает `broadcast_task_event("task.deleted")`
|
|
||||||
- [ ] События содержат полную информацию о задаче
|
|
||||||
- [ ] WebSocket клиент получает события в формате `{"type": "task.created", "data": task}`
|
|
||||||
|
|
||||||
**Компоненты:** Tracker
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 1.3: Добавить REST token auth для агентов
|
|
||||||
**Как** агент **я хочу** аутентифицироваться через токен в REST API **чтобы** делать запросы напрямую к Tracker
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Middleware `verify_agent_token()` проверяет заголовок `Authorization: Bearer tb-xxxxx`
|
|
||||||
- [ ] Токен валидируется по таблице Member.token
|
|
||||||
- [ ] Защищены все эндпоинты кроме `/docs`, `/health`, `/ws`
|
|
||||||
- [ ] При неверном токене возвращается 401
|
|
||||||
- [ ] BFF продолжает работать (проксирует с токеном)
|
|
||||||
|
|
||||||
**Компоненты:** Tracker
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
## Эпик 2: Task Operations — Агентские операции с задачами
|
|
||||||
Цель: Реализовать специальные операции для агентов из PRD
|
|
||||||
Зависимости: Эпик 1
|
|
||||||
|
|
||||||
### Story 2.1: Реализовать take task API
|
|
||||||
**Как** агент **я хочу** атомарно взять задачу в работу **чтобы** не было race condition с другими агентами
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/tasks/{id}/take` endpoint создан
|
|
||||||
- [ ] Проверяется что задача в статусе backlog или todo
|
|
||||||
- [ ] Атомарно устанавливается assignee_slug = текущий агент и status = in_progress
|
|
||||||
- [ ] При конфликте возвращается 409 Conflict
|
|
||||||
- [ ] Broadcast task.updated события после успешного взятия
|
|
||||||
- [ ] BFF проксирует операцию
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 2.2: Реализовать reject task API
|
|
||||||
**Как** агент **я хочу** отклонить назначенную мне задачу **чтобы** вернуть её в backlog с объяснением
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/tasks/{id}/reject` с body `{"reason": "string"}`
|
|
||||||
- [ ] Проверяется что assignee_slug = текущий агент
|
|
||||||
- [ ] Устанавливается assignee_slug = null, status = backlog
|
|
||||||
- [ ] Добавляется комментарий-сообщение "Задача отклонена: {reason}"
|
|
||||||
- [ ] Broadcast task.updated события
|
|
||||||
- [ ] BFF проксирует операцию
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 2.3: Реализовать assign task API
|
|
||||||
**Как** участник **я хочу** назначить задачу другому участнику **чтобы** делегировать работу
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/tasks/{id}/assign` с body `{"assignee_slug": "string"}`
|
|
||||||
- [ ] Проверяется существование assignee в проекте
|
|
||||||
- [ ] Устанавливается assignee_slug и status = todo (если был backlog)
|
|
||||||
- [ ] Broadcast task.updated события
|
|
||||||
- [ ] BFF проксирует операцию
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
### Story 2.4: Реализовать watch/unwatch task API
|
|
||||||
**Как** участник **я хочу** подписываться на уведомления по задаче **чтобы** отслеживать её прогресс
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/tasks/{id}/watch` добавляет текущий slug в watchers[]
|
|
||||||
- [ ] `DELETE /api/v1/tasks/{id}/watch` удаляет slug из watchers[]
|
|
||||||
- [ ] Дубликаты в watchers игнорируются
|
|
||||||
- [ ] Broadcast task.updated события при изменении watchers
|
|
||||||
- [ ] BFF проксирует операции
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
## Эпик 3: WebSocket Events — Правильная фильтрация событий
|
|
||||||
Цель: Унифицировать фильтрацию событий для всех типов участников
|
|
||||||
Зависимости: Эпик 2
|
|
||||||
|
|
||||||
### Story 3.1: Реализовать project.subscribe для humans
|
|
||||||
**Как** пользователь веб-клиента **я хочу** подписываться только на события нужных проектов **чтобы** не получать лишний трафик
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Humans/bridges могут отправлять `project.subscribe` сообщения
|
|
||||||
- [ ] `connection.subscribed_projects` хранит список подписок
|
|
||||||
- [ ] По умолчанию подписок нет (не "получать всё")
|
|
||||||
- [ ] События фильтруются по подпискам для всех типов участников
|
|
||||||
- [ ] Web Client автоматически подписывается на текущий проект
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / Web
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 3.2: Унифицировать фильтрацию по listen_mode
|
|
||||||
**Как** агент с chat_listen="mentions" **я хочу** получать только сообщения где меня упоминают **чтобы** снизить шум в канале
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Функция `should_receive_event(client, event_type, data)` для всех типов
|
|
||||||
- [ ] chat_listen="mentions" → только сообщения с mentions[] содержащими агента
|
|
||||||
- [ ] chat_listen="all" → все сообщения в подписанных проектах
|
|
||||||
- [ ] task_listen="mentions" → только задачи где агент assignee/reviewer/watcher
|
|
||||||
- [ ] task_listen="all" → все task события в подписанных проектах
|
|
||||||
- [ ] Humans используют ту же логику с listen_mode="all" по умолчанию
|
|
||||||
|
|
||||||
**Компоненты:** Tracker
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 3.3: Исправить on_behalf_of validation
|
|
||||||
**Как** BFF **я хочу** валидировать on_behalf_of пользователей **чтобы** предотвратить коллизии slug
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] При on_behalf_of проверяется существование Member с таким slug
|
|
||||||
- [ ] Несуществующие users получают префикс `web-{slug}`
|
|
||||||
- [ ] Все proxy подключения логируются с уровнем INFO
|
|
||||||
- [ ] Bridge не может перехватить slug существующего агента
|
|
||||||
- [ ] Добавлены unit тесты для collision cases
|
|
||||||
|
|
||||||
**Компоненты:** BFF
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
## Эпик 4: Agent Management — UI для управления агентами
|
|
||||||
Цель: Создать интерфейс для создания и мониторинга агентов
|
|
||||||
Зависимости: Эпик 3
|
|
||||||
|
|
||||||
### Story 4.1: Страница Agent Management
|
|
||||||
**Как** администратор **я хочу** видеть список всех агентов с их статусами **чтобы** мониторить работу системы
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Страница `/agents` доступна в навигации
|
|
||||||
- [ ] Таблица агентов с колонками: name, slug, status, capabilities, last_seen_at
|
|
||||||
- [ ] Статусы показаны цветами: 🟢 online, 🔴 offline, 🟡 busy
|
|
||||||
- [ ] Обновление статусов через WebSocket в реальном времени
|
|
||||||
- [ ] Кнопка "Create Agent" ведёт на форму создания
|
|
||||||
|
|
||||||
**Компоненты:** Web
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 4.2: Форма создания агента
|
|
||||||
**Как** администратор **я хочу** создать нового агента и получить его токен **чтобы** подключить к системе
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Форма с полями: name, slug, capabilities (чекбоксы), chat_listen, task_listen, model
|
|
||||||
- [ ] Slug валидируется на уникальность
|
|
||||||
- [ ] При создании генерируется токен формата `tb-{32 random chars}`
|
|
||||||
- [ ] Токен показывается один раз с предупреждением "Сохраните токен"
|
|
||||||
- [ ] После создания редирект на `/agents` со списком
|
|
||||||
- [ ] Токен копируется в clipboard кнопкой
|
|
||||||
|
|
||||||
**Компоненты:** Web / BFF / Tracker
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 4.3: Agent configuration UI
|
|
||||||
**Как** администратор **я хочу** редактировать настройки агента **чтобы** корректировать его поведение
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] В таблице агентов кнопка "Edit" открывает модальное окно
|
|
||||||
- [ ] Поля: name, capabilities, chat_listen, task_listen, model, prompt
|
|
||||||
- [ ] Slug не редактируется (readonly)
|
|
||||||
- [ ] Кнопка "Regenerate Token" с подтверждением
|
|
||||||
- [ ] Изменения сохраняются через PATCH API
|
|
||||||
- [ ] Обновление конфигурации применяется без перезапуска агента
|
|
||||||
|
|
||||||
**Компоненты:** Web / BFF / Tracker
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
## Эпик 5: Task Steps UI — Интеграция steps в интерфейс
|
|
||||||
Цель: Показывать прогресс задач через steps UI
|
|
||||||
Зависимости: Эпик 4
|
|
||||||
|
|
||||||
### Story 5.1: Steps в TaskModal
|
|
||||||
**Как** пользователь **я хочу** видеть этапы задачи в виде чеклиста **чтобы** отслеживать прогресс выполнения
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Секция Steps между описанием и комментариями в TaskModal
|
|
||||||
- [ ] Список шагов с чекбоксами и заголовками
|
|
||||||
- [ ] Прогресс-бар "3 из 7 выполнено" вверху секции
|
|
||||||
- [ ] Чекбоксы интерактивны — клик отмечает/снимает выполнение
|
|
||||||
- [ ] Обновления сохраняются через PATCH API
|
|
||||||
- [ ] Live обновления через WebSocket
|
|
||||||
|
|
||||||
**Компоненты:** Web
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 5.2: Управление steps в TaskModal
|
|
||||||
**Как** исполнитель задачи **я хочу** добавлять и удалять этапы **чтобы** планировать свою работу
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Кнопка "+ Add Step" в секции Steps
|
|
||||||
- [ ] Инлайн добавление шага с автосохранением
|
|
||||||
- [ ] Кнопка удаления у каждого шага (только для незавершённых)
|
|
||||||
- [ ] Drag & drop для изменения порядка шагов
|
|
||||||
- [ ] Завершённые шаги визуально отличаются (зачёркнутый текст)
|
|
||||||
|
|
||||||
**Компоненты:** Web
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
## Эпик 6: Files & Attachments — Загрузка файлов
|
|
||||||
Цель: Реализовать upload/download файлов в сообщениях
|
|
||||||
Зависимости: Эпик 5
|
|
||||||
|
|
||||||
### Story 6.1: Upload API для attachments
|
|
||||||
**Как** пользователь **я хочу** прикреплять файлы к сообщениям **чтобы** делиться документами с командой
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `POST /api/v1/messages/{id}/attachments` принимает multipart/form-data
|
|
||||||
- [ ] Файлы сохраняются в `/var/lib/team-board/attachments/{message_id}/`
|
|
||||||
- [ ] Создаётся Attachment запись в БД с metadata
|
|
||||||
- [ ] Поддерживаются файлы до 10MB
|
|
||||||
- [ ] Валидация MIME типов (изображения, документы, код)
|
|
||||||
- [ ] BFF проксирует загрузку с авторизацией
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 6.2: Download API для attachments
|
|
||||||
**Как** пользователь **я хочу** скачивать прикреплённые файлы **чтобы** просматривать их локально
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `GET /api/v1/attachments/{id}` отдаёт файл с правильными заголовками
|
|
||||||
- [ ] Content-Type, Content-Length, Content-Disposition установлены
|
|
||||||
- [ ] Проверка доступа — пользователь должен иметь доступ к сообщению
|
|
||||||
- [ ] Обслуживание статичных файлов через FastAPI
|
|
||||||
- [ ] BFF проксирует скачивание с авторизацией
|
|
||||||
|
|
||||||
**Компоненты:** Tracker / BFF
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
### Story 6.3: File preview в UI
|
|
||||||
**Как** пользователь **я хочу** видеть превью файлов в чате **чтобы** не скачивать их для просмотра
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Изображения показываются как thumbnail в сообщении
|
|
||||||
- [ ] Клик на thumbnail открывает полноразмерное изображение
|
|
||||||
- [ ] Документы показываются как ссылки с иконками типов файлов
|
|
||||||
- [ ] Код файлы (.js, .py, .md) показываются с syntax highlighting
|
|
||||||
- [ ] Кнопка скачивания у каждого attachment
|
|
||||||
|
|
||||||
**Компоненты:** Web
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
## Эпик 7: MCP Tools — Инструменты для агентов
|
|
||||||
Цель: Создать MCP Tools для взаимодействия агентов с системой
|
|
||||||
Зависимости: Эпик 6
|
|
||||||
|
|
||||||
### Story 7.1: Базовые Task Tools
|
|
||||||
**Как** агент **я хочу** создавать и управлять задачами через MCP **чтобы** автоматизировать workflow
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `create_task(title, description, project_slug, priority?, labels?)` → task_id
|
|
||||||
- [ ] `take_task(task_id)` → success/error
|
|
||||||
- [ ] `update_task(task_id, fields)` → updated task
|
|
||||||
- [ ] `complete_task(task_id)` → устанавливает status=done
|
|
||||||
- [ ] `list_tasks(project_slug?, status?, assignee?)` → task list
|
|
||||||
- [ ] Все tools используют существующий REST API с токен авторизацией
|
|
||||||
|
|
||||||
**Компоненты:** Picogent
|
|
||||||
**Оценка:** M
|
|
||||||
|
|
||||||
### Story 7.2: Communication Tools
|
|
||||||
**Как** агент **я хочу** отправлять сообщения и читать чат **чтобы** участвовать в обсуждениях
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `send_message(content, chat_id?, task_id?, mentions?)` → message_id
|
|
||||||
- [ ] `list_messages(chat_id?, task_id?, limit?)` → message list
|
|
||||||
- [ ] `add_task_comment(task_id, content)` → comment message
|
|
||||||
- [ ] `mention_user(slug, message)` → автодобавление в mentions[]
|
|
||||||
- [ ] Все сообщения получают правильный author_slug агента
|
|
||||||
|
|
||||||
**Компоненты:** Picogent
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
### Story 7.3: Project & Member Tools
|
|
||||||
**Как** агент **я хочу** получать информацию о проектах и участниках **чтобы** понимать контекст работы
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] `list_projects()` → project list с основными полями
|
|
||||||
- [ ] `get_project(slug)` → детальная информация о проекте
|
|
||||||
- [ ] `list_members(project_slug?)` → список участников
|
|
||||||
- [ ] `get_member(slug)` → информация об участнике
|
|
||||||
- [ ] `watch_task(task_id)` / `unwatch_task(task_id)` → управление подписками
|
|
||||||
|
|
||||||
**Компоненты:** Picogent
|
|
||||||
**Оценка:** S
|
|
||||||
|
|
||||||
## Эпик 8: Advanced Features — Дополнительные возможности
|
|
||||||
Цель: Реализовать отложенные features для полноценной работы
|
|
||||||
Зависимости: Эпик 7
|
|
||||||
|
|
||||||
### Story 8.1: Telegram Bridge Foundation
|
|
||||||
**Как** пользователь **я хочу** получать уведомления в Telegram **чтобы** не пропускать важные события проекта
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Bridge агент подключается как member типа "bridge"
|
|
||||||
- [ ] Принимает все события проектов (listen_mode="all")
|
|
||||||
- [ ] Фильтрует важные события: task.assigned на меня, mentions в чате
|
|
||||||
- [ ] Отправляет уведомления в личный чат Telegram
|
|
||||||
- [ ] Конфигурация через переменные окружения
|
|
||||||
|
|
||||||
**Компоненты:** Picogent (отдельный bridge agent)
|
|
||||||
**Оценка:** L
|
|
||||||
|
|
||||||
### Story 8.2: Voice Messages Support
|
|
||||||
**Как** пользователь **я хочу** отправлять голосовые сообщения **чтобы** быстро комментировать задачи
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] Web Client записывает аудио через MediaRecorder API
|
|
||||||
- [ ] Upload аудио как attachment с типом audio/*
|
|
||||||
- [ ] Message.voice_url ссылается на attachment
|
|
||||||
- [ ] Аудио плеер в UI для воспроизведения
|
|
||||||
- [ ] Агенты могут транскрибировать voice_url через STT
|
|
||||||
|
|
||||||
**Компоненты:** Web / Tracker / Picogent
|
|
||||||
**Оценка:** L
|
|
||||||
|
|
||||||
### Story 8.3: Advanced Task Dependencies
|
|
||||||
**Как** PM **я хочу** видеть граф зависимостей задач **чтобы** планировать последовательность работ
|
|
||||||
|
|
||||||
**Критерии приёмки:**
|
|
||||||
- [ ] UI для добавления зависимостей в TaskModal (autocomplete других задач)
|
|
||||||
- [ ] Валидация циклических зависимостей на бэкенде
|
|
||||||
- [ ] Визуализация зависимостей в виде граф-диаграммы
|
|
||||||
- [ ] Блокировка взятия задач с незавершёнными зависимостями
|
|
||||||
- [ ] Автоматическое уведомление при разблокировке задачи
|
|
||||||
|
|
||||||
**Компоненты:** Web / Tracker
|
|
||||||
**Оценка:** L
|
|
||||||
322
PRD.md
322
PRD.md
@ -1,322 +0,0 @@
|
|||||||
# Product Requirements Document — Team Board
|
|
||||||
|
|
||||||
**Версия:** 1.0
|
|
||||||
**Дата:** 2026-02-23
|
|
||||||
**Автор:** Mary (BMAD Method)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Видение продукта
|
|
||||||
|
|
||||||
Team Board — платформа для совместной работы людей и AI-агентов над проектами. Канбан-доска, чат, файлы — где агенты являются полноценными участниками команды: берут задачи, общаются, создают подзадачи, ревьюят код. Человек — участник процесса, а не наблюдатель. Всё происходит на человеческом языке в прозрачном чате, в отличие от MetaGPT и Claude Flow где агенты работают вместо людей.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Целевая аудитория
|
|
||||||
|
|
||||||
- **Разработчики** — управление проектами с AI-помощью
|
|
||||||
- **Product Manager** — координация команды из людей и агентов
|
|
||||||
- **Solo разработчики** — масштабирование через AI-агентов
|
|
||||||
- **Команды** — прозрачная работа с AI как с коллегами
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Ключевые концепции
|
|
||||||
|
|
||||||
### Member (Участники)
|
|
||||||
Единая модель для людей и агентов. Различие только в `type` ("human" | "agent") и методе авторизации (password/JWT vs token).
|
|
||||||
|
|
||||||
### Project (Проекты)
|
|
||||||
Контейнер для задач с канбан-доской, чатом и настройками. Поддержка multi-repo через `repo_urls[]`.
|
|
||||||
|
|
||||||
### Task (Задачи)
|
|
||||||
Основная единица работы со статусами backlog → todo → in_progress → in_review → done. Поддержка подзадач, зависимостей, наблюдателей.
|
|
||||||
|
|
||||||
### Step (Этапы)
|
|
||||||
Чеклист внутри задачи для отображения прогресса агента. В отличие от подзадач, этапы не попадают на канбан-доску.
|
|
||||||
|
|
||||||
### Message (Сообщения)
|
|
||||||
Unified модель для чат-сообщений И комментариев к задачам. Одно поле `chat_id` или `task_id` определяет контекст.
|
|
||||||
|
|
||||||
### Chat (Чаты)
|
|
||||||
Два типа: lobby (глобальный чат) и project (чат проекта). Комментарии к задачам через Message с `task_id`.
|
|
||||||
|
|
||||||
### Agent (AI-агенты)
|
|
||||||
Независимые приложения, подключающиеся по WebSocket. Настраиваются через AgentConfig с capabilities, listen modes, промптами.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Функциональные требования
|
|
||||||
|
|
||||||
### 4.1 Управление проектами
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| P001 | `GET /api/v1/projects` — список проектов | ✅ реализовано |
|
|
||||||
| P002 | `POST /api/v1/projects` — создание проекта с name, slug, description, repo_urls[] | ✅ реализовано |
|
|
||||||
| P003 | `PATCH /api/v1/projects/{slug}` — обновление проекта | ✅ реализовано |
|
|
||||||
| P004 | `DELETE /api/v1/projects/{slug}` — удаление проекта | ✅ реализовано |
|
|
||||||
| P005 | Поддержка multi-repo проектов через repo_urls[] | ✅ реализовано |
|
|
||||||
| P006 | Статусы проектов: active, archived | ✅ реализовано |
|
|
||||||
| P007 | CreateProjectModal в веб-интерфейсе | ✅ реализовано |
|
|
||||||
|
|
||||||
### 4.2 Управление задачами (Kanban)
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| T001 | `GET /api/v1/tasks` с фильтрами project_id, status, assignee, labels | ✅ реализовано |
|
|
||||||
| T002 | `POST /api/v1/tasks` — создание задачи с автогенерацией номера (TE-1, TE-2...) | ✅ реализовано |
|
|
||||||
| T003 | `PATCH /api/v1/tasks/{id}` — обновление полей задачи | ✅ реализовано |
|
|
||||||
| T004 | `DELETE /api/v1/tasks/{id}` — удаление задачи | ✅ реализовано |
|
|
||||||
| T005 | Статусы: backlog, todo, in_progress, in_review, done с свободными переходами | ✅ реализовано |
|
|
||||||
| T006 | Приоритеты: critical, high, medium, low | ✅ реализовано |
|
|
||||||
| T007 | Лейблы как string[] (coding, backend, urgent...) | ✅ реализовано |
|
|
||||||
| T008 | Подзадачи через parent_id (полноценные задачи на канбане) | ✅ реализовано |
|
|
||||||
| T009 | Зависимости через depends_on UUID[] | ✅ реализовано |
|
|
||||||
| T010 | Назначение assignee_slug, reviewer_slug | ✅ реализовано |
|
|
||||||
| T011 | Watchers как string[] (наблюдатели задачи) | ✅ реализовано |
|
|
||||||
| T012 | KanbanBoard с drag & drop и mobile tabs | ✅ реализовано |
|
|
||||||
| T013 | TaskModal с редактированием title, description, status, priority, assignee | ✅ реализовано |
|
|
||||||
| T014 | CreateTaskModal с выбором типа, приоритета, назначения | ✅ реализовано |
|
|
||||||
| T015 | `POST /api/v1/tasks/{id}/take` — атомарное взятие задачи агентом | ❌ не реализовано |
|
|
||||||
| T016 | `POST /api/v1/tasks/{id}/reject` — отклонение задачи с причиной | ❌ не реализовано |
|
|
||||||
| T017 | `POST /api/v1/tasks/{id}/assign` — назначение задачи | ❌ не реализовано |
|
|
||||||
| T018 | `POST /api/v1/tasks/{id}/watch` — подписка на задачу | ❌ не реализовано |
|
|
||||||
|
|
||||||
### 4.3 Чат и коммуникация
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| C001 | `GET /api/v1/messages` с фильтрами chat_id, task_id, limit, offset | ✅ реализовано |
|
|
||||||
| C002 | `POST /api/v1/messages` — отправка в чат или комментарий к задаче | ✅ реализовано |
|
|
||||||
| C003 | Unified Message модель для чат-сообщений И комментариев задач | ✅ реализовано |
|
|
||||||
| C004 | Упоминания через mentions[] с автодополнением @slug | ✅ реализовано |
|
|
||||||
| C005 | Поддержка threads через parent_id (только в чатах) | ✅ реализовано |
|
|
||||||
| C006 | Значки авторов в UI: 👤 human, 🤖 agent, ⚙️ system | ✅ реализовано |
|
|
||||||
| C007 | ChatPanel для lobby чата | ✅ реализовано |
|
|
||||||
| C008 | Комментарии к задачам в TaskModal | ✅ реализовано |
|
|
||||||
| C009 | Голосовые сообщения через voice_url | 🔨 частично |
|
|
||||||
| C010 | WebSocket chat.send для real-time отправки | ✅ реализовано |
|
|
||||||
|
|
||||||
### 4.4 AI агенты
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| A001 | `GET /api/v1/members` — список участников (люди + агенты) | ✅ реализовано |
|
|
||||||
| A002 | `POST /api/v1/members` — создание агента с токеном | ✅ реализовано |
|
|
||||||
| A003 | Unified Member модель с type ("human" | "agent") | ✅ реализовано |
|
|
||||||
| A004 | AgentConfig с capabilities[], chat_listen, task_listen, prompt, model | ✅ реализовано |
|
|
||||||
| A005 | Listen modes: "all" | "mentions" для chat и task событий раздельно | ✅ реализовано |
|
|
||||||
| A006 | WebSocket auth через token для агентов | ✅ реализовано |
|
|
||||||
| A007 | Heartbeat каждые 30 секунд, timeout 90 секунд → offline | ✅ реализовано |
|
|
||||||
| A008 | Статусы: online, offline, busy с broadcast уведомлениями | ✅ реализовано |
|
|
||||||
| A009 | Picogent агент готов к подключению (agent.json конфиг) | ✅ реализовано |
|
|
||||||
| A010 | Agent management UI — создание, настройка токенов | ❌ не реализовано |
|
|
||||||
| A011 | MCP Tools для агентов (create_task, take_task, send_message...) | ❌ не реализовано |
|
|
||||||
| A012 | Фильтрация WS событий по capabilities и listen modes | ❌ не реализовано |
|
|
||||||
|
|
||||||
### 4.5 Файлы и вложения
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| F001 | Attachment модель связанная с Message | ✅ реализовано |
|
|
||||||
| F002 | `POST /api/v1/messages/{id}/attachments` — загрузка файлов | ❌ не реализовано |
|
|
||||||
| F003 | `GET /api/v1/attachments/{id}` — скачивание файлов | ❌ не реализовано |
|
|
||||||
| F004 | Хранение файлов на диске по storage_path | ❌ не реализовано |
|
|
||||||
| F005 | Превью файлов в UI (изображения, код) | ❌ не реализовано |
|
|
||||||
|
|
||||||
### 4.6 Аутентификация и авторизация
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| AU001 | JWT аутентификация для людей (login/password) | ✅ реализовано |
|
|
||||||
| AU002 | Token аутентификация для агентов | ✅ реализовано |
|
|
||||||
| AU003 | BFF с JWT авторизацией на порту 8200 | ✅ реализовано |
|
|
||||||
| AU004 | Роли: owner, member, observer, bridge | ✅ реализовано |
|
|
||||||
| AU005 | AuthGuard компонент для защиты страниц | ✅ реализовано |
|
|
||||||
| AU006 | Login страница с формой входа | ✅ реализовано |
|
|
||||||
| AU007 | Token auth для REST API агентов | ❌ не реализовано |
|
|
||||||
|
|
||||||
### 4.7 WebSocket и реал-тайм
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| WS001 | WebSocket эндпоинт `/ws` с auth handshake | ✅ реализовано |
|
|
||||||
| WS002 | Сообщения: auth, heartbeat, chat.send, project.subscribe, ack | ✅ реализовано |
|
|
||||||
| WS003 | События: auth.ok/error, message.new, agent.status | ✅ реализовано |
|
|
||||||
| WS004 | BFF WebSocket прокси с on_behalf_of для browser пользователей | ✅ реализовано |
|
|
||||||
| WS005 | Очередь сообщений до auth.ok на клиенте | ✅ реализовано |
|
|
||||||
| WS006 | Project subscriptions для получения событий проекта | ✅ реализовано |
|
|
||||||
| WS007 | Task события: task.created, task.updated, task.assigned | ❌ не реализовано |
|
|
||||||
| WS008 | Фильтрация событий по listen_mode и capabilities | ❌ не реализовано |
|
|
||||||
|
|
||||||
### 4.8 Steps (этапы задач)
|
|
||||||
|
|
||||||
| ID | Описание | Статус |
|
|
||||||
|----|----------|--------|
|
|
||||||
| S001 | Step модель связанная с Task | ✅ реализовано |
|
|
||||||
| S002 | `GET /api/v1/tasks/{id}/steps` — список этапов | ✅ реализовано |
|
|
||||||
| S003 | `POST /api/v1/tasks/{id}/steps` — создание этапа | ✅ реализовано |
|
|
||||||
| S004 | `PATCH /api/v1/tasks/{tid}/steps/{sid}` — обновление (title, done) | ✅ реализовано |
|
|
||||||
| S005 | `DELETE /api/v1/tasks/{tid}/steps/{sid}` — удаление этапа | ✅ реализовано |
|
|
||||||
| S006 | Steps UI в TaskModal с чекбоксами и прогрессом | ✅ реализовано |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Нефункциональные требования
|
|
||||||
|
|
||||||
### Производительность
|
|
||||||
- WebSocket подключения: до 100 одновременных агентов
|
|
||||||
- REST API: до 1000 RPS
|
|
||||||
- База данных: PostgreSQL с connection pooling
|
|
||||||
- Время отклика API: <200ms для CRUD операций
|
|
||||||
|
|
||||||
### Безопасность
|
|
||||||
- JWT токены с истечением 30 дней
|
|
||||||
- Agent токены формата `tb-{32 random chars}`
|
|
||||||
- Все пароли хешируются bcrypt
|
|
||||||
- HTTPS обязателен в продакшене
|
|
||||||
- CORS настроен для фронтенда
|
|
||||||
|
|
||||||
### Масштабируемость
|
|
||||||
- Архитектура готова к горизонтальному масштабированию
|
|
||||||
- Статичные файлы и аттачменты на диске (возможно S3 в будущем)
|
|
||||||
- Redis для кеширования и очередей (зарезервировано)
|
|
||||||
- WebSocket connections можно балансировать
|
|
||||||
|
|
||||||
### Надёжность
|
|
||||||
- Агенты переподключаются при обрыве WebSocket
|
|
||||||
- Heartbeat мониторинг с автоматическим offline
|
|
||||||
- База данных с автоматическими backups (планируется)
|
|
||||||
- Graceful shutdown всех сервисов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Архитектура (высокоуровневая)
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
```
|
|
||||||
Web Client (Next.js, порт 3100)
|
|
||||||
↓ HTTPS
|
|
||||||
BFF (Python/FastAPI, порт 8200)
|
|
||||||
↓ HTTP + WebSocket
|
|
||||||
Tracker (Python/FastAPI, порт 8100)
|
|
||||||
↓ SQL
|
|
||||||
PostgreSQL (порт 5433)
|
|
||||||
|
|
||||||
Agents (Picogent) → WebSocket → Tracker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Принципы
|
|
||||||
- **Tracker-Centric:** Tracker — внутренний сервис, единая точка правды
|
|
||||||
- **BFF для веба:** Только веб-клиент ходит через BFF, агенты — напрямую
|
|
||||||
- **WebSocket для событий:** Реал-тайм уведомления и чат
|
|
||||||
- **REST для мутаций:** Все изменения данных через REST API
|
|
||||||
- **Unified модели:** Member = human | agent, Message = chat | task comments
|
|
||||||
|
|
||||||
### Деплой
|
|
||||||
- **Tracker:** Docker Compose с PostgreSQL
|
|
||||||
- **BFF + Web Client:** systemd сервисы на хосте
|
|
||||||
- **Agents:** systemd unit на агента (один процесс = один агент)
|
|
||||||
- **Nginx:** SSL termination, статичные файлы, проксирование
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Текущий статус
|
|
||||||
|
|
||||||
### Что работает сейчас (✅)
|
|
||||||
|
|
||||||
**Backend (Tracker v0.2.0):**
|
|
||||||
- Все модели данных созданы и соответствуют архитектуре
|
|
||||||
- Полный REST API CRUD для всех сущностей
|
|
||||||
- WebSocket с базовой функциональностью (auth, heartbeat, chat.send)
|
|
||||||
- JWT и token аутентификация
|
|
||||||
- БД инициализация с admin пользователем
|
|
||||||
|
|
||||||
**Frontend (Web Client v0.2.0):**
|
|
||||||
- KanbanBoard с drag & drop и mobile tabs
|
|
||||||
- TaskModal с steps, комментариями, редактированием
|
|
||||||
- ChatPanel для lobby чата
|
|
||||||
- CreateTaskModal, CreateProjectModal
|
|
||||||
- BFF прокси с JWT авторизацией
|
|
||||||
- AuthGuard и Login страница
|
|
||||||
|
|
||||||
**Infrastructure:**
|
|
||||||
- Docker Compose для разработки (Tracker + PostgreSQL + Redis)
|
|
||||||
- systemd сервисы для продакшена (BFF, Web Client)
|
|
||||||
- Nginx конфигурация с SSL
|
|
||||||
- Домены: dev.team.uix.su (активный), team.uix.su (placeholder)
|
|
||||||
|
|
||||||
**Agent System:**
|
|
||||||
- Picogent готов к подключению с agent.json конфигом
|
|
||||||
- WebSocket подключение и аутентификация работают
|
|
||||||
- Heartbeat и статус мониторинг
|
|
||||||
|
|
||||||
### Что не работает (❌)
|
|
||||||
|
|
||||||
**WebSocket события:**
|
|
||||||
- Task events (task.created, task.updated, task.assigned) не транслируются
|
|
||||||
- Фильтрация событий по listen_mode и capabilities не реализована
|
|
||||||
- Humans и bridges пока получают ВСЕ события (упрощённо)
|
|
||||||
|
|
||||||
**Agent операции:**
|
|
||||||
- MCP Tools для агентов отсутствуют
|
|
||||||
- take_task, reject_task, assign_task API не реализованы
|
|
||||||
- Agent management UI не создан
|
|
||||||
|
|
||||||
**Файлы:**
|
|
||||||
- Upload/download API для attachments не реализован
|
|
||||||
- Превью файлов в UI отсутствует
|
|
||||||
|
|
||||||
**REST API для агентов:**
|
|
||||||
- Token авторизация работает только для WebSocket, не для REST
|
|
||||||
|
|
||||||
### Известные баги
|
|
||||||
|
|
||||||
**WebSocket (исправлено сегодня):**
|
|
||||||
- ✅ connect() не вызывался — исправлено
|
|
||||||
- ✅ subscribeProject() не вызывался — исправлено
|
|
||||||
- ✅ send() дропал сообщения до auth.ok — исправлено очередью
|
|
||||||
|
|
||||||
**BFF WebSocket proxy:**
|
|
||||||
- ✅ Добавлен on_behalf_of чтобы browser-пользователь регистрировался по своему slug
|
|
||||||
|
|
||||||
**Архитектура WS упрощена:**
|
|
||||||
- ✅ Humans/bridges получают ВСЕ события без подписок, фильтрация на клиенте
|
|
||||||
- ✅ Agents получают по подпискам project.subscribe
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Приоритеты следующей итерации
|
|
||||||
|
|
||||||
### Phase 1: Завершить WebSocket v2 (MUST)
|
|
||||||
1. **Task events через WS** — task.created/updated/assigned с фильтрацией
|
|
||||||
2. **Listen mode фильтрация** — chat_listen и task_listen работают корректно
|
|
||||||
3. **Humans подписки** — убрать "получают всё", добавить project.subscribe
|
|
||||||
|
|
||||||
### Phase 2: Agent Management UI (SHOULD)
|
|
||||||
1. **Agent management страница** — создание агентов с генерацией токенов
|
|
||||||
2. **Agent мониторинг** — список агентов со статусами и capabilities
|
|
||||||
3. **Token display** — показать токен один раз при создании агента
|
|
||||||
|
|
||||||
### Phase 3: MCP Tools для агентов (SHOULD)
|
|
||||||
1. **Basic MCP Tools** — create_task, take_task, update_task, send_message
|
|
||||||
2. **REST Token auth** — token проверка для REST API агентов
|
|
||||||
3. **Picogent интеграция** — подключить реального агента к системе
|
|
||||||
|
|
||||||
### Phase 4: Files & Attachments (COULD)
|
|
||||||
1. **Upload/download API** — POST /messages/{id}/attachments, GET /attachments/{id}
|
|
||||||
2. **File storage** — сохранение на диск по storage_path
|
|
||||||
3. **File preview UI** — превью изображений и кода в сообщениях
|
|
||||||
|
|
||||||
### Nice to Have (будущие итерации)
|
|
||||||
- Telegram Bridge для дублирования чатов
|
|
||||||
- Advanced dashboard с метриками проектов
|
|
||||||
- OAuth/Authentik интеграция для мультипользователей
|
|
||||||
- Git workflow интеграция с Merge Requests
|
|
||||||
- Session compaction для длинных агентских сессий
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Документ создан на основе полного анализа 24+ файлов документации Team Board и текущего состояния кода. Приоритет у реального состояния системы над планами в документах.**
|
|
||||||
95
README.md
95
README.md
@ -1,95 +0,0 @@
|
|||||||
# Team Board
|
|
||||||
|
|
||||||
Платформа для управления AI-агентами с проектами и канбан-досками.
|
|
||||||
|
|
||||||
## Концепция
|
|
||||||
|
|
||||||
Несколько AI-агентов работают над проектами как команда:
|
|
||||||
- Каждый проект = Git репозиторий + канбан-доска + чат
|
|
||||||
- Задачи с вложенными подзадачами (без потери контекста)
|
|
||||||
- Агенты общаются через задачи — всё прозрачно для человека
|
|
||||||
- Разные провайдеры: Claude, Codex, Gemini, OpenClaw
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
Микросервисы:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ FRONTEND (Next.js) │
|
|
||||||
│ - Проекты, доски, задачи, чаты │
|
|
||||||
│ - Authentik OAuth │
|
|
||||||
└──────────────────────────┬──────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ API GATEWAY │
|
|
||||||
│ - Маршрутизация запросов │
|
|
||||||
│ - Аутентификация │
|
|
||||||
└──────────────────────────┬──────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────────┼─────────────────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
||||||
│ Projects │ │ Tasks │ │ Agents │
|
|
||||||
│ Service │ │ Service │ │ Service │
|
|
||||||
│ (Python) │ │ (Python) │ │ (Python) │
|
|
||||||
└─────────────┘ └─────────────┘ └─────────────┘
|
|
||||||
│ │ │
|
|
||||||
└─────────────────┼─────────────────┘
|
|
||||||
▼
|
|
||||||
┌─────────────┐
|
|
||||||
│ PostgreSQL │
|
|
||||||
└─────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Сервисы
|
|
||||||
|
|
||||||
| Сервис | Порт | Описание |
|
|
||||||
|--------|------|----------|
|
|
||||||
| frontend | 3000 | Next.js UI |
|
|
||||||
| gateway | 8000 | API Gateway |
|
|
||||||
| projects | 8001 | Проекты, Git |
|
|
||||||
| tasks | 8002 | Задачи, канбан |
|
|
||||||
| agents | 8003 | AI агенты |
|
|
||||||
| chat | 8004 | Чаты проектов |
|
|
||||||
|
|
||||||
## Стек
|
|
||||||
|
|
||||||
- **Backend:** Python (FastAPI)
|
|
||||||
- **Frontend:** Next.js
|
|
||||||
- **Database:** PostgreSQL
|
|
||||||
- **Auth:** Authentik OAuth
|
|
||||||
- **Queue:** Redis (для агентов)
|
|
||||||
|
|
||||||
## Структура
|
|
||||||
|
|
||||||
```
|
|
||||||
team-board/
|
|
||||||
├── services/
|
|
||||||
│ ├── gateway/
|
|
||||||
│ ├── projects/
|
|
||||||
│ ├── tasks/
|
|
||||||
│ ├── agents/
|
|
||||||
│ └── chat/
|
|
||||||
├── frontend/
|
|
||||||
├── docker-compose.yml
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Разработка
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск всех сервисов
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Только backend
|
|
||||||
cd services/tasks && uvicorn app:app --reload
|
|
||||||
|
|
||||||
# Только frontend
|
|
||||||
cd frontend && npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Домен
|
|
||||||
|
|
||||||
https://team.uix.su
|
|
||||||
300
TASKS.md
300
TASKS.md
@ -1,300 +0,0 @@
|
|||||||
# TASKS.md — Полный список задач Team Board
|
|
||||||
|
|
||||||
Обновлено: 2026-02-28
|
|
||||||
|
|
||||||
## Легенда
|
|
||||||
- ✅ Готово
|
|
||||||
- 🔧 В работе
|
|
||||||
- 📋 Запланировано
|
|
||||||
- 💡 Идея (требует brainstorm)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Инфраструктура
|
|
||||||
|
|
||||||
- ✅ Tracker MVP: FastAPI + SQLAlchemy 2 + PostgreSQL 16
|
|
||||||
- ✅ Docker для Tracker (bind mounts: postgres, uploads, projects)
|
|
||||||
- ✅ Nginx + SSL (dev.team.uix.su)
|
|
||||||
- ✅ CI/CD: Gitea Actions auto-deploy
|
|
||||||
- ✅ Redis удалён (был не нужен)
|
|
||||||
- ✅ BFF удалён — фронт напрямую к Tracker
|
|
||||||
|
|
||||||
## Аутентификация
|
|
||||||
|
|
||||||
- ✅ JWT auth для людей (login/password)
|
|
||||||
- ✅ Token auth для агентов (Bearer tb-xxx)
|
|
||||||
- ✅ Auth middleware на всех /api/ endpoints
|
|
||||||
- ✅ OPTIONS preflight пропускается
|
|
||||||
- ✅ Bcrypt пароли (миграция с SHA-256)
|
|
||||||
- 📋 Authentik OAuth (SSO через auth.uix.su)
|
|
||||||
|
|
||||||
## Модели данных
|
|
||||||
|
|
||||||
- ✅ Unified Member model (human + agent)
|
|
||||||
- ✅ UUID как primary identifier везде (slug только для display)
|
|
||||||
- ✅ Soft-delete для agents (is_active flag)
|
|
||||||
- ✅ AgentConfig: capabilities, labels, chat/task_listen, prompt, model, provider
|
|
||||||
- ✅ Project: name, slug, description, repo_urls, status, task_counter, auto_assign
|
|
||||||
- ✅ ProjectMember (many-to-many)
|
|
||||||
- ✅ Task: title, desc, type, status, priority, labels, assignee, reviewer, watchers, parent_id, depends_on
|
|
||||||
- ✅ TaskAction (structured audit log)
|
|
||||||
- ✅ TaskLink (blocks, depends_on, relates_to)
|
|
||||||
- ✅ Label (глобальные, app-wide)
|
|
||||||
- ✅ TaskLabel (many-to-many)
|
|
||||||
- ✅ Step (checklist внутри задачи)
|
|
||||||
- ✅ Chat + Message (unified: чат + комментарии к задачам)
|
|
||||||
- ✅ Message: thinking, tool_log, actor_id, mentions
|
|
||||||
- ✅ Attachment (файлы в чате)
|
|
||||||
- ✅ ProjectFile (файлы проекта)
|
|
||||||
- ✅ Centralized enums (tracker/enums.py)
|
|
||||||
|
|
||||||
## WebSocket
|
|
||||||
|
|
||||||
- ✅ WS Foundation: подключение, auth, heartbeat
|
|
||||||
- ✅ Auto-subscribe на проекты при auth
|
|
||||||
- ✅ broadcast_message (с фильтрацией агентов)
|
|
||||||
- ✅ broadcast_task_event (с project_id)
|
|
||||||
- ✅ System messages: @mention filtering для агентов
|
|
||||||
- ✅ Agent streaming: start/delta/tool/end events
|
|
||||||
- ✅ config.updated event (hot reload)
|
|
||||||
- ✅ Frontend WS heartbeat (30 сек)
|
|
||||||
|
|
||||||
## REST API
|
|
||||||
|
|
||||||
- ✅ CRUD: projects, tasks, steps, messages, members
|
|
||||||
- ✅ Task links API (POST/GET/DELETE)
|
|
||||||
- ✅ Labels API (global CRUD + task associations)
|
|
||||||
- ✅ Project files API (upload, download, search, thumbnails)
|
|
||||||
- ✅ Chat attachments API
|
|
||||||
- ✅ Task search (q= по номеру + title)
|
|
||||||
- ✅ Subscription modes: all, mentions, assigned, none
|
|
||||||
- ✅ Auto-assign (task labels ↔ agent labels, project option)
|
|
||||||
- ✅ Init handshake: assigned_tasks в auth.ok
|
|
||||||
|
|
||||||
## Web Client (Vite + React SPA)
|
|
||||||
|
|
||||||
### Kanban Board
|
|
||||||
- ✅ 5 колонок (backlog → done) с цветами
|
|
||||||
- ✅ Карточки: приоритет, assignee, labels
|
|
||||||
- ✅ Parent/child key на карточках (TE-1 / TE-3)
|
|
||||||
- ✅ Move buttons (← →)
|
|
||||||
- ✅ Task dedup fix
|
|
||||||
|
|
||||||
### Task Modal
|
|
||||||
- ✅ Inline edit title + description
|
|
||||||
- ✅ Task key сверху (breadcrumb с родителем)
|
|
||||||
- ✅ Status, priority, assignee, reviewer selectors
|
|
||||||
- ✅ Steps (checklist с прогресс-баром)
|
|
||||||
- ✅ Comments (newest first, input on top)
|
|
||||||
- ✅ File attachments в чате
|
|
||||||
- ✅ Dependencies section (colored labels + search autocomplete)
|
|
||||||
- ✅ Subtasks section (status dots, clickable navigation)
|
|
||||||
- ✅ "+" Подзадача button
|
|
||||||
- ✅ Thinking blocks (collapsible 💭)
|
|
||||||
- ✅ Tool log (collapsible 🔧)
|
|
||||||
|
|
||||||
### Create Task Modal
|
|
||||||
- ✅ Title, Description
|
|
||||||
- ✅ Type (task/bug/feature)
|
|
||||||
- ✅ Status selector (colored circles, default backlog)
|
|
||||||
- ✅ Priority selector (colored circles)
|
|
||||||
- ✅ Assignee + Reviewer dropdowns
|
|
||||||
- ✅ Labels toggle buttons
|
|
||||||
- ✅ Parent ID (for subtasks)
|
|
||||||
|
|
||||||
### Chat Panel
|
|
||||||
- ✅ Fullscreen, dedup by ID
|
|
||||||
- ✅ Message styling (yellow=system, green=agent)
|
|
||||||
- ✅ @mention autocomplete (MentionInput)
|
|
||||||
- ✅ File attachments
|
|
||||||
|
|
||||||
### Project
|
|
||||||
- ✅ Tabs: Board / Chat / Files / Settings
|
|
||||||
- ✅ Project Files (drag & drop, search, thumbnails)
|
|
||||||
- ✅ Project Settings: name, desc, repos, status, auto-assign toggle
|
|
||||||
- ✅ Member management (humans + agent toggles)
|
|
||||||
|
|
||||||
### Settings (app-wide)
|
|
||||||
- ✅ Settings Layout с sidebar
|
|
||||||
- ✅ 🏷️ Лейблы — глобальные лейблы (CRUD, color picker)
|
|
||||||
- ✅ 🤖 Агенты — список, создание, редактирование
|
|
||||||
- ✅ Agent Modal: capabilities, labels, listen modes, prompt, model, token management
|
|
||||||
|
|
||||||
### Auth & Navigation
|
|
||||||
- ✅ Login page
|
|
||||||
- ✅ Sidebar с проектами
|
|
||||||
- ✅ Mobile viewport fix (100dvh)
|
|
||||||
- ✅ No-cache для index.html
|
|
||||||
|
|
||||||
## Picogent (Pi Agent Core — TypeScript)
|
|
||||||
|
|
||||||
- ✅ WS transport с auth
|
|
||||||
- ✅ Router: pure relay, zero side effects
|
|
||||||
- ✅ Agent streaming (start/delta/tool/end)
|
|
||||||
- ✅ Config split: agent.json + config.json
|
|
||||||
- ✅ Hot config update (config.updated handler)
|
|
||||||
- ✅ Anti-loop: ignore agent messages unless @mentioned
|
|
||||||
- ✅ Bootstrap context: AGENT.md + memory files
|
|
||||||
- ✅ Session compaction via Haiku
|
|
||||||
- ✅ Per-project memory (UUID-based dirs)
|
|
||||||
- ✅ Auto-reply (if no send_message tool called)
|
|
||||||
- ✅ 23 MCP tools: tasks(5), steps(2), messages(2), projects(2), members(1), files(6), task links(3)
|
|
||||||
- ✅ systemd template: picogent@.service
|
|
||||||
|
|
||||||
## Агенты
|
|
||||||
|
|
||||||
- ✅ Coder: slug=coder, capabilities=[code,review], AGENT.md (BMAD Dev style)
|
|
||||||
- ✅ Architect: slug=architect, capabilities=[architecture,review,planning], AGENT.md (BMAD Winston)
|
|
||||||
- ✅ Anti-loop rules в обоих AGENT.md
|
|
||||||
- ✅ Multi-agent: max 1 exchange per topic
|
|
||||||
|
|
||||||
## Безопасность
|
|
||||||
|
|
||||||
- ✅ Path traversal protection (basename + realpath)
|
|
||||||
- ✅ Agent sandbox (allowedPaths = [agentHome])
|
|
||||||
- ✅ CASCADE delete (tasks → messages, steps)
|
|
||||||
- ✅ Auth на всех mutations (get_current_member Depends)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Запланировано
|
|
||||||
|
|
||||||
### Фича: RBAC — система прав
|
|
||||||
- 💡 Brainstorm: архитектура permissions (roles, per-endpoint checks)
|
|
||||||
- 📋 Реализация: таблица roles, role_permissions, middleware
|
|
||||||
- 📋 Права для агентов (ограничение действий)
|
|
||||||
- 📋 Viewer role (read-only, уже решено концептуально)
|
|
||||||
|
|
||||||
### Фича: Subtasks (расширение)
|
|
||||||
- ✅ parent_id в модели Task
|
|
||||||
- ✅ Subtasks section в TaskModal
|
|
||||||
- ✅ Create subtask button
|
|
||||||
- 📋 Subtasks на kanban доске (индентация или группировка)
|
|
||||||
- 📋 Drag & drop subtask в другого родителя
|
|
||||||
|
|
||||||
### Фича: Task Priorities (расширение)
|
|
||||||
- ✅ Priority field в модели + UI
|
|
||||||
- 📋 Сортировка по приоритету на доске
|
|
||||||
- 📋 Фильтр по приоритету
|
|
||||||
|
|
||||||
### Фича: PR-flow + Gitea интеграция
|
|
||||||
- 💡 Brainstorm: архитектура интеграции с Gitea API
|
|
||||||
- 📋 requires_pr flag на задаче
|
|
||||||
- 📋 Webhook от Gitea → Tracker (PR created/merged)
|
|
||||||
- 📋 Auto-move задачи в review при PR creation
|
|
||||||
- 📋 Auto-move в done при PR merge
|
|
||||||
|
|
||||||
### Фича: Review Iterations
|
|
||||||
- 📋 author ≠ reviewer validation
|
|
||||||
- 📋 Max 3 review iterations
|
|
||||||
- 📋 Review comments → task comments
|
|
||||||
|
|
||||||
### Фича: Voice Messages
|
|
||||||
- 📋 Web: MediaRecorder → upload audio
|
|
||||||
- 📋 Audio player в чате
|
|
||||||
- 📋 STT транскрипция для агентов (через Thoth)
|
|
||||||
- 📋 Android: voice recording
|
|
||||||
|
|
||||||
### Фича: Telegram Bridge (Picobridge)
|
|
||||||
- 📋 Bridge agent (member_type=bridge)
|
|
||||||
- 📋 WS → Telegram notifications
|
|
||||||
- 📋 Важные события: assigned, mentions
|
|
||||||
- 📋 Bidirectional: Telegram → Tracker
|
|
||||||
|
|
||||||
### Фича: Android Client
|
|
||||||
- ✅ Brainstorm завершён (Kotlin + Jetpack Compose)
|
|
||||||
- 📋 Создать репо team-board/android
|
|
||||||
- 📋 MVP: Kanban + Chat + Voice + Task creation
|
|
||||||
|
|
||||||
### Фича: Authentik OAuth
|
|
||||||
- 📋 OAuth2 flow с auth.uix.su
|
|
||||||
- 📋 Auto-create member при первом login
|
|
||||||
- 📋 SSO для всех сервисов
|
|
||||||
|
|
||||||
### Фича: Billing / API Usage
|
|
||||||
- 💡 Brainstorm: что считать, как хранить
|
|
||||||
- 📋 Tracking token usage per agent
|
|
||||||
- 📋 Cost dashboard
|
|
||||||
|
|
||||||
### Фича: Расширенные зависимости
|
|
||||||
- ✅ TaskLink model (blocks, depends_on, relates_to)
|
|
||||||
- ✅ UI: colored labels + search autocomplete
|
|
||||||
- 📋 Валидация циклических зависимостей
|
|
||||||
- 📋 Блокировка взятия задач с незавершёнными зависимостями
|
|
||||||
- 📋 Граф зависимостей (визуализация)
|
|
||||||
|
|
||||||
### Фича: Файлы в задачах
|
|
||||||
- 📋 Attachments прямо на задаче (не только в чате)
|
|
||||||
- 📋 Drag & drop в TaskModal
|
|
||||||
|
|
||||||
### Фича: Picogent npm package
|
|
||||||
- 📋 bin entry, shebang
|
|
||||||
- 📋 Publish в Gitea registry
|
|
||||||
|
|
||||||
### Фича: Фильтры и поиск
|
|
||||||
- 📋 Фильтр задач по assignee, priority, labels на доске
|
|
||||||
- 📋 Глобальный поиск задач
|
|
||||||
- 📋 Saved filters
|
|
||||||
|
|
||||||
### Фича: Vector Search (Phase 3)
|
|
||||||
- 💡 Когда >20 проектов — RAG для agent memory
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Исследования и заимствования
|
|
||||||
|
|
||||||
### Google A2A Protocol — что можно взять
|
|
||||||
Источник: https://github.com/a2aproject/A2A (RC v1.0)
|
|
||||||
|
|
||||||
- 💡 **context_id** — кросс-задачный контекст для группировки связанных задач. Агент помнит что TE-5, TE-6, TE-7 — одна история. Наследуется от родителя.
|
|
||||||
- 💡 **Extensions framework** — механизм плагинов для агентов:
|
|
||||||
- Data-only: метаинформация (стоимость, SLA)
|
|
||||||
- Profile: ограничения на формат данных
|
|
||||||
- Method Extensions: кастомные API endpoints (git-diff, run-tests)
|
|
||||||
- State Machine: кастомные статусы задач
|
|
||||||
- 💡 **Agent Card** (`/.well-known/agent.json`) — стандартный формат описания агента для совместимости с внешними A2A-агентами
|
|
||||||
- 💡 **Artifact vs Message** — разделение: Message = общение, Artifact = результат работы
|
|
||||||
- 💡 **A2A Gateway** — адаптер для интеграции с внешними A2A-совместимыми агентами
|
|
||||||
|
|
||||||
### Фича: Agent Skills (Inter-agent Method Invocation)
|
|
||||||
Вдохновлено: A2A Method Extensions
|
|
||||||
|
|
||||||
- 💡 `skills` поле в AgentConfig — массив объектов {id, name, description, input_schema, output_schema}
|
|
||||||
- 💡 API: `GET /agents/{id}/skills` — список skills агента
|
|
||||||
- 💡 API: `POST /agents/{id}/skills/{skill_id}/invoke` — вызов skill
|
|
||||||
- 💡 WS event `skill.invoke` → агент получает структурированный запрос
|
|
||||||
- 💡 WS event `skill.result` → результат возвращается вызывающему
|
|
||||||
- 💡 MCP tool `list_agent_skills(agent)` — агент узнаёт skills других агентов
|
|
||||||
- 💡 MCP tool `invoke_skill(agent, skill_id, params)` — формальный вызов
|
|
||||||
- 💡 Discovery: skills включаются в auth.ok (bootstrap) для всех участников проекта
|
|
||||||
- 💡 Tracker = прокси (маршрутизирует invoke → целевому агенту через WS)
|
|
||||||
- 💡 Input/output schema validation на Tracker (до отправки агенту)
|
|
||||||
|
|
||||||
### Фича: Agent Secrets & Granular Permissions
|
|
||||||
Связано с: RBAC, Agent Skills
|
|
||||||
|
|
||||||
- 💡 **Agent Secrets** — зашифрованное хранилище секретов per-agent (API keys, prod credentials, SSH keys)
|
|
||||||
- Секреты доступны ТОЛЬКО агенту-владельцу (не через UI, не другим агентам)
|
|
||||||
- Tracker хранит encrypted, агент расшифровывает при получении
|
|
||||||
- Управление: admin создаёт/обновляет через UI, агент читает через MCP tool `get_secret(key)`
|
|
||||||
- 💡 **Resource Permissions** — какой агент к чему имеет доступ:
|
|
||||||
- `prod_deploy` — только deploy-agent
|
|
||||||
- `db_write` — только coder (read-only для остальных)
|
|
||||||
- `external_api` — только у агента с соответствующим секретом
|
|
||||||
- 💡 **Sandbox levels** — расширение текущего allowedPaths:
|
|
||||||
- Level 0: только свой agentHome (текущее)
|
|
||||||
- Level 1: + project files (read)
|
|
||||||
- Level 2: + project files (write) + git
|
|
||||||
- Level 3: + system commands (deploy, restart)
|
|
||||||
- 💡 Предпосылка для Agent Skills — без permissions skills бесполезны
|
|
||||||
|
|
||||||
### Фича: Token Budget & Error Recovery
|
|
||||||
- 📋 Picogent: graceful error handling при 429/402 от LLM API
|
|
||||||
- Системное сообщение в чат: "⚠️ Агент X: rate limit / billing error"
|
|
||||||
- Статус агента → `error` (новый MemberStatus)
|
|
||||||
- Retry с backoff для 429, полная остановка для 402
|
|
||||||
- 📋 Token usage tracking: Picogent считает usage → отправляет в Tracker
|
|
||||||
- 📋 UI: расход токенов per agent (dashboard)
|
|
||||||
- 📋 Alerts: уведомление при приближении к лимиту (80%, 95%)
|
|
||||||
- 💡 Budget limits: max tokens per agent per day/month
|
|
||||||
- 💡 Auto-pause: агент автоматически останавливается при достижении лимита
|
|
||||||
1727
TRACKER-PROTOCOL.md
1727
TRACKER-PROTOCOL.md
File diff suppressed because it is too large
Load Diff
1
agent-coder
Submodule
1
agent-coder
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 054690b080840ee0d34d72f3fcc8ac4d1698b8dc
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,192 +0,0 @@
|
|||||||
# Интеграция агентов — ресёрч
|
|
||||||
|
|
||||||
## Аналогия с GitLab Runners
|
|
||||||
|
|
||||||
| GitLab | Team Board |
|
|
||||||
|--------|------------|
|
|
||||||
| Runner | Agent Instance |
|
|
||||||
| Runner Token | Agent Token |
|
|
||||||
| `.gitlab-ci.yml` | Контракт (contract.json) |
|
|
||||||
| Job | Task |
|
|
||||||
| Executor (docker/shell) | Adapter (OpenClaw/Anthropic/CLI) |
|
|
||||||
| Runner registration | `POST /api/v1/agents/register` |
|
|
||||||
| Runner tags | Agent capabilities |
|
|
||||||
|
|
||||||
## Архитектура подключения
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Админ создаёт Agent в UI → получает registration token
|
|
||||||
2. На сервере запускается Agent Instance с этим токеном
|
|
||||||
3. Agent Instance регистрируется: POST /ws?token=<reg_token>
|
|
||||||
4. Tracker подтверждает → выдаёт session token
|
|
||||||
5. Agent слушает задачи через WebSocket
|
|
||||||
6. При получении задачи → выполняет через адаптер
|
|
||||||
```
|
|
||||||
|
|
||||||
## Контракт агента (contract)
|
|
||||||
|
|
||||||
Файл, описывающий что агент умеет и как с ним взаимодействовать.
|
|
||||||
Хранится в трекере, привязан к конкретному агенту.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "1",
|
|
||||||
"capabilities": ["coding", "review", "docs"],
|
|
||||||
"max_concurrent_tasks": 2,
|
|
||||||
"timeout_seconds": 600,
|
|
||||||
"accept_task_types": ["coding", "review"],
|
|
||||||
"tools": {
|
|
||||||
"file_read": true,
|
|
||||||
"file_write": true,
|
|
||||||
"git_commit": true,
|
|
||||||
"git_pr": true,
|
|
||||||
"web_search": false,
|
|
||||||
"shell_exec": false
|
|
||||||
},
|
|
||||||
"limits": {
|
|
||||||
"max_file_size_mb": 10,
|
|
||||||
"max_files_per_task": 20,
|
|
||||||
"max_comment_length": 5000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Операции агента (через WebSocket)
|
|
||||||
|
|
||||||
Агент должен уметь:
|
|
||||||
|
|
||||||
### Задачи
|
|
||||||
| Операция | WS Event | Описание |
|
|
||||||
|----------|----------|----------|
|
|
||||||
| Получить задачу | `task.assigned` (входящее) | Трекер назначает задачу агенту |
|
|
||||||
| Взять задачу | `task.take` | Агент сам берёт задачу из TODO |
|
|
||||||
| Обновить статус | `task.status` | draft→ready→in_progress→review→completed |
|
|
||||||
| Обновить описание | `task.update` | Изменить title, description |
|
|
||||||
| Создать задачу | `task.create` | Агент создаёт подзадачу |
|
|
||||||
| Создать подзадачу | `task.create` (с parent_id) | Декомпозиция |
|
|
||||||
| Добавить зависимость | `task.dependency.add` | TEA-3 depends on TEA-1 |
|
|
||||||
|
|
||||||
### Комментарии
|
|
||||||
| Операция | WS Event | Описание |
|
|
||||||
|----------|----------|----------|
|
|
||||||
| Добавить комментарий | `task.comment` | Текст, code snippets |
|
|
||||||
| Читать комментарии | `task.comments.list` | История обсуждения |
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
| Операция | WS Event / HTTP | Описание |
|
|
||||||
|----------|-----------------|----------|
|
|
||||||
| Загрузить файл | `POST /api/v1/tasks/{id}/files` | Результат работы |
|
|
||||||
| Скачать файл | `GET /api/v1/tasks/{id}/files/{fid}` | Входные данные |
|
|
||||||
| Список файлов | `task.files.list` | Что есть в задаче |
|
|
||||||
|
|
||||||
### Чат
|
|
||||||
| Операция | WS Event | Описание |
|
|
||||||
|----------|----------|----------|
|
|
||||||
| Сообщение в чат | `chat.send` | Обсуждение |
|
|
||||||
| Читать чат | `chat.history` | Контекст |
|
|
||||||
|
|
||||||
## Страница настройки в UI
|
|
||||||
|
|
||||||
### /settings/agents
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Агенты [+ Создать] │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ ● Кодер online 2 tasks │
|
|
||||||
│ claude-opus coding, review │
|
|
||||||
│ Подключён: 2 часа назад │
|
|
||||||
│ │
|
|
||||||
│ ● Аналитик offline │
|
|
||||||
│ gpt-4 analytics, docs │
|
|
||||||
│ Последний раз: вчера │
|
|
||||||
│ │
|
|
||||||
│ ○ Тестировщик не зарегистрирован │
|
|
||||||
│ Токен: tb-agent-abc123... [Копировать] │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Создание агента
|
|
||||||
1. Имя + slug
|
|
||||||
2. Системный промпт (роль)
|
|
||||||
3. Контракт (capabilities, tools, limits)
|
|
||||||
4. Генерация registration token
|
|
||||||
5. Инструкция по запуску:
|
|
||||||
```bash
|
|
||||||
team-board-agent register \
|
|
||||||
--tracker wss://team.uix.su/ws \
|
|
||||||
--token tb-agent-abc123... \
|
|
||||||
--adapter anthropic \
|
|
||||||
--api-key sk-ant-...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Регистрация и аутентификация
|
|
||||||
|
|
||||||
### Шаг 1: Создание (UI)
|
|
||||||
- Админ создаёт агента в UI
|
|
||||||
- Генерируется одноразовый registration token
|
|
||||||
- Токен показывается один раз
|
|
||||||
|
|
||||||
### Шаг 2: Регистрация (Agent Instance)
|
|
||||||
```
|
|
||||||
Agent → Tracker: ws connect + registration token
|
|
||||||
Tracker: validates token, marks agent as registered
|
|
||||||
Tracker → Agent: session token (долгоживущий)
|
|
||||||
Agent: сохраняет session token локально
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 3: Работа
|
|
||||||
```
|
|
||||||
Agent → Tracker: ws connect + session token
|
|
||||||
Tracker: validates, marks online
|
|
||||||
Agent ←→ Tracker: events (tasks, chat, files)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Отзыв токена
|
|
||||||
- Админ в UI нажимает "Отозвать"
|
|
||||||
- Tracker закрывает WebSocket
|
|
||||||
- Agent не может переподключиться
|
|
||||||
- Нужна новая регистрация
|
|
||||||
|
|
||||||
## SDK (Python)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from team_board import Agent, AnthropicAdapter
|
|
||||||
|
|
||||||
agent = Agent(
|
|
||||||
tracker_url="wss://team.uix.su/ws",
|
|
||||||
token="tb-session-xyz...",
|
|
||||||
)
|
|
||||||
|
|
||||||
@agent.on_task
|
|
||||||
async def handle_task(task):
|
|
||||||
# Читаем описание и файлы
|
|
||||||
files = await task.list_files()
|
|
||||||
|
|
||||||
# Работаем через адаптер
|
|
||||||
result = await agent.adapter.complete(
|
|
||||||
system=agent.system_prompt,
|
|
||||||
messages=[{"role": "user", "content": task.description}],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Добавляем комментарий
|
|
||||||
await task.comment(result.text)
|
|
||||||
|
|
||||||
# Загружаем файл
|
|
||||||
await task.upload_file("result.py", result.code)
|
|
||||||
|
|
||||||
# Меняем статус
|
|
||||||
await task.set_status("review")
|
|
||||||
|
|
||||||
agent.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Приоритеты реализации
|
|
||||||
|
|
||||||
1. [ ] Tracker: token generation + WS auth
|
|
||||||
2. [ ] Tracker: agent events (task.take, task.status, task.comment, task.create)
|
|
||||||
3. [ ] Tracker: file upload/download API
|
|
||||||
4. [ ] UI: /settings/agents page
|
|
||||||
5. [ ] SDK: Python package (team-board-agent)
|
|
||||||
6. [ ] SDK: AnthropicAdapter + OpenClawAdapter
|
|
||||||
7. [ ] Первый агент: тестовый (echo/summarize)
|
|
||||||
8. [ ] Первый реальный агент: Кодер (через OpenClaw/Claude)
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
# Архитектурный ревью: поиск противоречий и проблем
|
|
||||||
Дата: 2026-02-22
|
|
||||||
Автор: Winston (Архитектор, BMAD)
|
|
||||||
|
|
||||||
Проанализированы все документы: ARCHITECTURE.md, 10 брейнштормов, WS-PROTOCOL, MCP-TOOLS.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔴 Критические противоречия
|
|
||||||
|
|
||||||
### 1. Redis Streams: решено или нет?
|
|
||||||
- **BRAINSTORM-MICROSERVICES**: Redis Streams = "КЛЮЧЕВОЕ решение", принят, расписан reconnect через XREAD
|
|
||||||
- **Разговор 22.02**: Хозяин сказал "не уверен что нужен Redis Streams, оставляй на обсуждение"
|
|
||||||
- **ARCHITECTURE.md**: Redis "пока не используется", но в "Идеях" — Redis Streams для event bus
|
|
||||||
- **BRAINSTORM-WS-V2**: Redis Streams не упоминается вообще
|
|
||||||
|
|
||||||
**Проблема:** Документы противоречат друг другу. В одном Redis Streams принят, в другом под вопросом.
|
|
||||||
|
|
||||||
**Решение:** Убрать Redis Streams из принятых решений. Оставить как идею на будущее. Сейчас: WS для online агентов + REST для получения пропущенного при reconnect (GET /tasks?assignee=me). Проще, меньше компонентов.
|
|
||||||
|
|
||||||
### 2. Unified Message vs ChatMessage + TaskComment
|
|
||||||
- **BRAINSTORM-UI**: решено — Message = одна сущность (chat_id? / task_id?)
|
|
||||||
- **BRAINSTORM-CHATS**: модель `ChatMessage` с `chat_id` (не task_id)
|
|
||||||
- **BRAINSTORM-TASKS**: модель `TaskComment` — отдельная сущность
|
|
||||||
- **ARCHITECTURE.md**: `ChatMessage` и комментарии описаны отдельно
|
|
||||||
|
|
||||||
**Проблема:** Старые документы описывают две сущности, новое решение — одну. ARCHITECTURE.md не обновлён.
|
|
||||||
|
|
||||||
**Решение:** Обновить ARCHITECTURE.md. Одна таблица `Message` с полями chat_id? и task_id?.
|
|
||||||
|
|
||||||
### 3. Файловое хранилище: три версии
|
|
||||||
- **ARCHITECTURE.md**: "Файловое хранилище — Per-project директории, Upload/download через REST API"
|
|
||||||
- **BRAINSTORM-AGENTS**: "File storage — всегда, docs, картинки, видео, attachments"
|
|
||||||
- **Разговор 22.02**: "Отдельного файлового сервиса нет, файлы = вложения к сообщениям/задачам"
|
|
||||||
|
|
||||||
**Проблема:** ARCHITECTURE.md описывает отдельный файловый сервис, а решение — просто attachments.
|
|
||||||
|
|
||||||
**Решение:** Обновить. Файлы = attachments в Message. Документация проекта = ссылка в описании (repo_urls / description). Per-project директории на диске для хранения attachment'ов — деталь реализации.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟡 Нестыковки (не критические)
|
|
||||||
|
|
||||||
### 4. WS-PROTOCOL.md vs BRAINSTORM-WS-V2
|
|
||||||
Два документа описывают WS протокол с разными решениями:
|
|
||||||
- **WS-PROTOCOL.md** (старый): task.take, task.complete, task.comment через WS (мутации!)
|
|
||||||
- **BRAINSTORM-WS-V2** (новый): мутации только через REST
|
|
||||||
|
|
||||||
**Решение:** WS-PROTOCOL.md устарел. Либо обновить, либо пометить deprecated и ссылаться на WS-V2.
|
|
||||||
|
|
||||||
### 5. Agent model: Member vs Agent
|
|
||||||
- **ARCHITECTURE.md**: Unified Member model (human + agent = одна модель)
|
|
||||||
- **WS-PROTOCOL.md**: отдельная модель `Agent` с полями connection_type, callback_url и т.д.
|
|
||||||
- **BRAINSTORM-AGENTS**: "Агент = чёрный ящик", отдельная сущность
|
|
||||||
|
|
||||||
**Проблема:** Не ясно — Member или Agent? Если Member, то connection_type, callback_url, prompt, capabilities — это поля Member?
|
|
||||||
|
|
||||||
**Решение:** Member — базовая модель. Для type=agent дополнительные поля (capabilities, listen_mode, prompt, model). Можно JSON-поле `agent_config` или отдельная таблица `AgentConfig` с FK на Member.
|
|
||||||
|
|
||||||
### 6. Listen modes: один или два?
|
|
||||||
- **BRAINSTORM-PROJECTS**: `listen: all | mentions` — один режим
|
|
||||||
- **BRAINSTORM-WS-V2**: `chat_listen` + `task_listen` — два раздельных
|
|
||||||
|
|
||||||
**Решение:** Два раздельных (WS-V2 — более новое решение).
|
|
||||||
|
|
||||||
### 7. HTTP Callback transport
|
|
||||||
- **WS-PROTOCOL.md**: описан HTTP Callback как альтернативный транспорт
|
|
||||||
- **Нигде больше не упоминается**
|
|
||||||
|
|
||||||
**Решение:** Убрать из MVP. Если агент offline — он при reconnect запросит текущее состояние через REST.
|
|
||||||
|
|
||||||
### 8. chat.send: через WS или REST?
|
|
||||||
- **BRAINSTORM-PROTOCOL**: "WS = real-time + чат"
|
|
||||||
- **BRAINSTORM-MCP-TOOLS**: `send_message` в списке MCP tools (= REST)
|
|
||||||
- **BRAINSTORM-WS-V2**: `chat.send` в списке клиент→сервер WS событий
|
|
||||||
|
|
||||||
**Проблема:** Чат отправляется и через WS, и через REST?
|
|
||||||
|
|
||||||
**Решение:** Оба варианта валидны. WS для агентов (уже подключены, real-time). REST (MCP tool) — тоже работает (для MCP-клиентов без WS). Tracker обрабатывает одинаково.
|
|
||||||
|
|
||||||
### 9. Watchers: новая фича, нигде кроме WS-V2
|
|
||||||
- **BRAINSTORM-WS-V2**: watchers[] на задаче
|
|
||||||
- **BRAINSTORM-TASKS**: не упоминается
|
|
||||||
- **ARCHITECTURE.md**: не упоминается
|
|
||||||
|
|
||||||
**Решение:** Добавить watchers в модель задачи при обновлении ARCHITECTURE.md.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟢 Открытые вопросы (не противоречия, а незакрытое)
|
|
||||||
|
|
||||||
### 10. Лейблы: глобальные или per-project?
|
|
||||||
Упоминается в BRAINSTORM-PROJECTS как TBD. Не решено.
|
|
||||||
|
|
||||||
### 11. Реакции в чатах
|
|
||||||
Упоминается в BRAINSTORM-CHATS как TBD. Не решено.
|
|
||||||
|
|
||||||
### 12. Multi-instance агентов
|
|
||||||
Упоминается в BRAINSTORM-MISC. Как различать: slug = роль, instance_id?
|
|
||||||
|
|
||||||
### 13. Rollback файлов в хранилище
|
|
||||||
Упоминается в BRAINSTORM-MISC. Версионирование attachments?
|
|
||||||
|
|
||||||
### 14. Устаревшие документы
|
|
||||||
- `WS-PROTOCOL.md` — устарел (заменён BRAINSTORM-WS-V2 + BRAINSTORM-PROTOCOL)
|
|
||||||
- `AGENTS-INTEGRATION.md` — не проверен на актуальность
|
|
||||||
- `RESEARCH.md` — исследование, может быть неактуально
|
|
||||||
- `IDEAS.md` — первоначальные идеи, многое уже решено иначе
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Рекомендации
|
|
||||||
|
|
||||||
1. **Обновить ARCHITECTURE.md v0.4** — единый источник правды, включить все решения из брейнштормов
|
|
||||||
2. **Пометить устаревшие документы** — WS-PROTOCOL.md, AGENTS-INTEGRATION.md
|
|
||||||
3. **Решить Redis Streams** — убрать из принятых, оставить как идею
|
|
||||||
4. **Решить открытые вопросы** (лейблы, реакции) или явно пометить как "решим при реализации"
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
# Брейншторм: механика подключения агентов к Team Board
|
|
||||||
Дата: 2026-02-20
|
|
||||||
|
|
||||||
## Архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
Хозяин ──голос──→ Марков ──HTTP API──→ Team Board (Tracker)
|
|
||||||
Хозяин ──веб────→ team.uix.su ──────→ ↑
|
|
||||||
Хозяин ──tg─────→ Telegram Bridge ──WS──→ │
|
|
||||||
│
|
|
||||||
Агент-Архитектор ──────────────────WS/HTTP──→ │
|
|
||||||
Агент-Кодер #1 ────────────────────WS/HTTP──→ │
|
|
||||||
Агент-Кодер #2 ────────────────────WS/HTTP──→ │
|
|
||||||
```
|
|
||||||
|
|
||||||
Все входы равноправны. Team Board — центральный хаб. Система работает без Маркова.
|
|
||||||
|
|
||||||
## Роли
|
|
||||||
|
|
||||||
| Роль | Кто | Что делает |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| Хозяин | Eugene | Ставит задачи, контролирует, одобряет MR |
|
|
||||||
| Марков | OpenClaw | Голосовой интерфейс хозяина, менеджер (необязателен) |
|
|
||||||
| Архитектор | Агент (Opus) | Декомпозиция фич → задачи, координация |
|
|
||||||
| Кодер | Агент (Sonnet) | Кодинг, MR, исправление конфликтов |
|
|
||||||
| Bridge | Telegram бот | Дублирует чат в Telegram и обратно |
|
|
||||||
|
|
||||||
## Один Runner — разные роли
|
|
||||||
- Один и тот же binary/процесс
|
|
||||||
- Роль определяется конфигом: модель, prompt, capabilities
|
|
||||||
- Скилл для Runner'ов описывает взаимодействие с Tracker
|
|
||||||
|
|
||||||
## Транспорт
|
|
||||||
- **WebSocket** — основной (persistent connection, события в реальном времени)
|
|
||||||
- **HTTP Callback** — альтернативный (Tracker POST'ит события на URL агента)
|
|
||||||
- **HTTP API** — для файлов, задач, регистрации
|
|
||||||
|
|
||||||
## Агент = чёрный ящик
|
|
||||||
- Получает сообщения / задачи
|
|
||||||
- Имеет LLM + tools (Read, Write, Edit, Bash, WebSearch...)
|
|
||||||
- Отвечает в чат, создаёт подзадачи, открывает MR
|
|
||||||
- Настройки (prompt, роль) на стороне агента
|
|
||||||
|
|
||||||
## Задачи
|
|
||||||
|
|
||||||
### Жизненный цикл
|
|
||||||
```
|
|
||||||
backlog → todo → in_progress → in_review → done
|
|
||||||
↑ ↓
|
|
||||||
└── rejected ──┘
|
|
||||||
```
|
|
||||||
- `backlog → todo` = назначение (человек или архитектор)
|
|
||||||
- `todo` = готова к работе, агент может взять
|
|
||||||
- `task.take` — атомарная операция (кто первый — того и тапки)
|
|
||||||
- Большая задача → агент сам разбивает на подзадачи
|
|
||||||
|
|
||||||
### Назначение
|
|
||||||
1. Человек назначает вручную
|
|
||||||
2. Архитектор создаёт и назначает
|
|
||||||
3. Агент сам берёт из `todo` по capabilities
|
|
||||||
4. Через чат: "ребята, новая задача"
|
|
||||||
|
|
||||||
## Git workflow
|
|
||||||
- Агент клонит repo в свою рабочую директорию
|
|
||||||
- Работает в отдельной ветке
|
|
||||||
- По завершении → **Merge Request**
|
|
||||||
- Ревью (агент/человек) → approve/reject
|
|
||||||
- **Конфликты**: решает тот, чей MR. После — повторный review.
|
|
||||||
- Автомерж после approve
|
|
||||||
|
|
||||||
## Файлы проекта — гибрид
|
|
||||||
- **Git repo** — опционально (привязывается если проект кодовый)
|
|
||||||
- **File storage** — всегда (docs, картинки, видео, attachments)
|
|
||||||
- Доступ через HTTP API (агент может быть на другом сервере)
|
|
||||||
- Чат и задачи поддерживают прикрепление файлов
|
|
||||||
|
|
||||||
## Проекты ≠ только код
|
|
||||||
- Кодовый проект: repo + files
|
|
||||||
- Документационный: только files
|
|
||||||
- Медийный: только files
|
|
||||||
|
|
||||||
## Контекст проекта
|
|
||||||
- Документация в `docs/` проекта или в file storage
|
|
||||||
- Все агенты в чате = все в контексте
|
|
||||||
- НЕ привязываемся к конкретному LLM (никаких CLAUDE.md)
|
|
||||||
- Скилл для Runner'ов описывает где что искать
|
|
||||||
|
|
||||||
## Проблема "глухого агента"
|
|
||||||
- LLM `query()` блокирующий — агент не слышит новых сообщений
|
|
||||||
- Решения: **короткие задачи** + **checkpoint pattern** (чекать между шагами)
|
|
||||||
|
|
||||||
## Чат = лента событий
|
|
||||||
- Сообщения людей + агентов + системные уведомления
|
|
||||||
- MR создан/approved/rejected → в чат
|
|
||||||
- Статус задачи изменился → в чат
|
|
||||||
- Можно фильтровать по типу
|
|
||||||
|
|
||||||
## Уведомления
|
|
||||||
- Всё в чат Team Board
|
|
||||||
- Telegram Bridge дублирует в Telegram группу
|
|
||||||
- Типы: task_created, task_updated, mr_created, mr_approved, agent_connected
|
|
||||||
|
|
||||||
## Безопасность
|
|
||||||
- Аутентификация через **токены** (прописываются заранее при регистрации агента)
|
|
||||||
- Каждый агент = уникальный токен
|
|
||||||
- Пока без OAuth/сложной авторизации
|
|
||||||
|
|
||||||
## Мониторинг
|
|
||||||
- **Heartbeat** — Tracker пингует агентов периодически
|
|
||||||
- Если агент не отвечает → статус `offline`
|
|
||||||
- Уведомление в чат: "Агент X перестал отвечать"
|
|
||||||
|
|
||||||
## Тестирование / Ревью
|
|
||||||
- Другие агенты проверяют результат (агент-ревьюер)
|
|
||||||
- CI/CD на MR (Gitea Actions) — автотесты
|
|
||||||
|
|
||||||
## Интеграция агентов с Tracker
|
|
||||||
- **MCP-сервер** для Tracker — нативные tools (create_task, send_message и т.д.)
|
|
||||||
- **Скилл (fallback)** — для моделей без MCP, инструкции с curl
|
|
||||||
- **Скилл для правил** — роль, стиль работы, когда что делать
|
|
||||||
- MCP = tools (ЧТО делать), Скилл = правила (КАК делать)
|
|
||||||
- Скилл можно автогенерировать из MCP-описания
|
|
||||||
|
|
||||||
## Открытые вопросы
|
|
||||||
- Формат скилла для Runner'ов (взаимодействие с Tracker)
|
|
||||||
- Как агент определяет релевантность задачи для себя?
|
|
||||||
- Автоматическое назначение по capabilities (будущее)
|
|
||||||
- Интеграция с Gitea API для MR
|
|
||||||
- UX веб-клиента (отдельная тема)
|
|
||||||
|
|
||||||
## Референсы
|
|
||||||
- **MetaGPT** (github.com/FoundationAgents/MetaGPT) — промпты ролей (PM, Architect, Engineer, QA), pub/sub между ролями, SOP. Забрать промпты для назначения ролей агентам.
|
|
||||||
- **Claude Flow / Ruflo** — claims, swarm топологии, multi-provider
|
|
||||||
- **BMAD Method** — brainstorming workflow, party mode (мульти-ролевой prompt)
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
# Брейншторм: Чаты
|
|
||||||
Дата: 2026-02-21
|
|
||||||
|
|
||||||
## Типы чатов
|
|
||||||
|
|
||||||
### Lobby (глобальный)
|
|
||||||
- Общий чат для всех агентов и людей
|
|
||||||
- Без файлового хранилища (бесконечный поток)
|
|
||||||
- Системные уведомления
|
|
||||||
|
|
||||||
### Project Chat (чат проекта)
|
|
||||||
- Per-project
|
|
||||||
- Обсуждение проекта, координация
|
|
||||||
- Можно отправлять файлы/документы
|
|
||||||
|
|
||||||
### Task Comments (комменты задачи)
|
|
||||||
- Per-task
|
|
||||||
- Обсуждение конкретной задачи
|
|
||||||
- Mentions (@slug) → уведомление агенту
|
|
||||||
- Голосовые комментарии (Thoth)
|
|
||||||
|
|
||||||
## Фичи
|
|
||||||
|
|
||||||
### ✅ Threads (потоки обсуждения)
|
|
||||||
Как в Slack — ответ на конкретное сообщение создаёт ветку.
|
|
||||||
Не засоряет основной чат. Работает в project chat и lobby.
|
|
||||||
|
|
||||||
### ✅ Голосовые сообщения
|
|
||||||
- Запись голоса → Thoth транскрибирует → текст + аудио сохраняются
|
|
||||||
- Работает в чатах и комментариях задач
|
|
||||||
|
|
||||||
### ✅ Файлы в чат
|
|
||||||
- Можно отправить документ/файл в сообщении
|
|
||||||
- В project chat и task comments (не в lobby)
|
|
||||||
|
|
||||||
### ⚠️ Реакции (emoji)
|
|
||||||
- Выглядит круто, добавляет живость
|
|
||||||
- Проблема: жрёт токены (агент видит реакцию → тратит токены на обработку)
|
|
||||||
- Варианты:
|
|
||||||
- Реакции не попадают в промпт агента (только visual в UI)
|
|
||||||
- Агент может ставить реакции через MCP tool (дёшево)
|
|
||||||
- Реакции попадают в промпт только если это @mention-reaction
|
|
||||||
- **Решение TBD**
|
|
||||||
|
|
||||||
## Модель данных
|
|
||||||
|
|
||||||
```
|
|
||||||
ChatMessage:
|
|
||||||
id: UUID
|
|
||||||
chat_id: UUID (lobby / project / task)
|
|
||||||
author_type: human | agent | system
|
|
||||||
author_slug: string
|
|
||||||
content: text (markdown)
|
|
||||||
thread_id: UUID | null (ответ в потоке)
|
|
||||||
mentions: string[]
|
|
||||||
voice_url: string | null
|
|
||||||
attachments: file[] | null
|
|
||||||
reactions: [{emoji, author_slug}] | null
|
|
||||||
created_at
|
|
||||||
```
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
# Брейншторм: Сводный индекс
|
|
||||||
Дата: 2026-02-20
|
|
||||||
|
|
||||||
## Обсуждённые темы
|
|
||||||
|
|
||||||
### ✅ Агентная архитектура
|
|
||||||
📄 `BRAINSTORM-AGENTS-2026-02-20.md`
|
|
||||||
- Система автономна (работает без Маркова)
|
|
||||||
- Один Runner binary (picogent), роль = конфиг
|
|
||||||
- Git workflow с MR, конфликты решает MR owner
|
|
||||||
- Файлы: гибрид (repo + file storage), доступ через HTTP API
|
|
||||||
- MCP для tools + Skill fallback
|
|
||||||
- Чат = лента событий (люди + агенты + системные)
|
|
||||||
- Checkpoint pattern для "глухого агента"
|
|
||||||
- Референсы: MetaGPT (промпты ролей), Claude Flow (claims)
|
|
||||||
|
|
||||||
### ✅ Задачи (Tasks)
|
|
||||||
📄 `BRAINSTORM-TASKS-2026-02-20.md`
|
|
||||||
- Поля: priority, labels, parent_id, depends_on, reviewer, type
|
|
||||||
- Свободные переходы статусов (любой → любой)
|
|
||||||
- Комментарии с mentions, threads, голосовые
|
|
||||||
- Агент может отклонить задачу
|
|
||||||
- Автообнаружение блокеров
|
|
||||||
- **Подзадачи vs Этапы**: подзадачи на доске, этапы внутри задачи
|
|
||||||
|
|
||||||
### ✅ Проекты (Projects)
|
|
||||||
📄 `BRAINSTORM-PROJECTS-2026-02-20.md`
|
|
||||||
- Вкладки: канбан, чат, дашборд, настройки, файлы, activity feed
|
|
||||||
- Multi-repo (массив repo_urls)
|
|
||||||
- Кросс-проектные ссылки
|
|
||||||
- Архив, проект без задач
|
|
||||||
- Онбординг агента, listen modes, label matching
|
|
||||||
|
|
||||||
### ✅ Picogent — агент-исполнитель
|
|
||||||
📄 Репо: `team-board/picogent`
|
|
||||||
- In-process agent loop (Pi Agent Core)
|
|
||||||
- Dual transport: HTTP + WebSocket
|
|
||||||
- Skills, sessions, sandbox, directory mode
|
|
||||||
- **Заменяет runner/** — более зрелая реализация
|
|
||||||
|
|
||||||
## Темы для обсуждения (не обсуждены)
|
|
||||||
|
|
||||||
### 🔲 Агенты (модель, роли, permissions)
|
|
||||||
- Регистрация и жизненный цикл
|
|
||||||
- Роли: owner, agent, bridge, observer
|
|
||||||
- Permissions: send_messages, create_tasks, assign_tasks...
|
|
||||||
- Состояния: online, offline, busy, error
|
|
||||||
- Heartbeat и мониторинг
|
|
||||||
- Auto-assign по capabilities + labels
|
|
||||||
|
|
||||||
### 🔲 Чаты
|
|
||||||
- Lobby (глобальный), project chat, task comments
|
|
||||||
- Как связаны (или не связаны) эти три типа
|
|
||||||
- Форматирование, markdown, code blocks
|
|
||||||
- Голосовые сообщения в чате
|
|
||||||
- Реакции на сообщения (emoji)
|
|
||||||
- Threads в чате (не только в комментариях)
|
|
||||||
|
|
||||||
### 🔲 Файловое хранилище
|
|
||||||
- Upload/download API
|
|
||||||
- Структура: per-project директории
|
|
||||||
- Связь с git repos
|
|
||||||
- Превью файлов (images, code)
|
|
||||||
- Версионирование (или нет?)
|
|
||||||
|
|
||||||
### 🔲 Telegram Bridge
|
|
||||||
- Один бот связывает Team Board ↔ Telegram группу
|
|
||||||
- Форматирование сообщений с именем отправителя
|
|
||||||
- Какие события пробрасывать
|
|
||||||
- Голосовые через Thoth
|
|
||||||
|
|
||||||
### ✅ Протокол Picogent ↔ Tracker
|
|
||||||
📄 `BRAINSTORM-PROTOCOL-2026-02-20.md`
|
|
||||||
- WS = real-time (события push + чат сообщения)
|
|
||||||
- REST = все мутации через MCP tools
|
|
||||||
- Router разделяет: "в промпт" vs "internal"
|
|
||||||
- Listen modes: all vs mentions-only
|
|
||||||
- Prompt guidelines для работы с задачами и коммуникации
|
|
||||||
|
|
||||||
### 🔲 WebSocket протокол v2 (реализация)
|
|
||||||
- Синхронизация picogent WS-протокола с tracker
|
|
||||||
- Auth, event dispatch, ack на стороне tracker
|
|
||||||
- Room subscriptions для агентов
|
|
||||||
|
|
||||||
### 🔲 MCP Tools для Tracker
|
|
||||||
- create_task, take_task, update_task, send_message
|
|
||||||
- list_tasks, get_task, upload_file
|
|
||||||
- Skill fallback для моделей без MCP
|
|
||||||
|
|
||||||
### 🔲 Web UI
|
|
||||||
- Task detail view (описание, комментарии, steps, attachments)
|
|
||||||
- Agent management (создание, настройки, мониторинг)
|
|
||||||
- Dashboard проекта
|
|
||||||
- Настройки проекта
|
|
||||||
|
|
||||||
### 🔲 CI/CD и деплой
|
|
||||||
- Как деплоить picogent (systemd? docker?)
|
|
||||||
- Автоматический restart при падении
|
|
||||||
- Логирование и мониторинг
|
|
||||||
- Scaling (несколько агентов одного типа)
|
|
||||||
|
|
||||||
### 🔲 Безопасность
|
|
||||||
- Что агент может/не может делать
|
|
||||||
- Allowed paths vs полный доступ
|
|
||||||
- Rate limiting
|
|
||||||
- Audit log (кто что сделал)
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
# Брейншторм: MCP Tools
|
|
||||||
Дата: 2026-02-22
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
- MCP Tools = REST API Tracker, вызываемый агентом
|
|
||||||
- Git — НЕ в MCP (агент работает через git CLI напрямую)
|
|
||||||
- Файлы приходят с сущностями, но отдельный list_files тоже есть
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
### Задачи
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `list_tasks` | Список задач (фильтры: project, status, assignee, labels) |
|
|
||||||
| `get_task` | Получить задачу по ID/key |
|
|
||||||
| `create_task` | Создать задачу |
|
|
||||||
| `update_task` | Обновить поля |
|
|
||||||
| `take_task` | Взять себе (атомарно) |
|
|
||||||
| `reject_task` | Отклонить с причиной |
|
|
||||||
| `assign_task` | Назначить другому |
|
|
||||||
| `delete_task` | Удалить |
|
|
||||||
|
|
||||||
### Steps (этапы внутри задачи)
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `add_step` | Добавить этап |
|
|
||||||
| `complete_step` | Завершить этап |
|
|
||||||
| `update_step` | Обновить текст |
|
|
||||||
|
|
||||||
### Сообщения (единая модель — чат + комменты)
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `send_message` | В чат (chat_id) или к задаче (task_id) |
|
|
||||||
| `reply_message` | Ответ в thread (parent_id) |
|
|
||||||
| `list_messages` | Список (по chat_id / task_id) |
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `upload_file` | Загрузить файл |
|
|
||||||
| `list_files` | Список по задаче/проекту |
|
|
||||||
| `download_file` | Скачать файл |
|
|
||||||
|
|
||||||
### Проекты
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `list_projects` | Список проектов |
|
|
||||||
| `get_project` | Информация о проекте |
|
|
||||||
|
|
||||||
### Участники
|
|
||||||
| Tool | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `list_members` | Кто в проекте |
|
|
||||||
| `update_status` | Обновить свой статус (online/busy) |
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
# Брейншторм: Микросервисные паттерны
|
|
||||||
Дата: 2026-02-21
|
|
||||||
|
|
||||||
## Контекст
|
|
||||||
Team Board = микросервисная архитектура: Tracker (ядро), Picogent (агенты), Bridges (Telegram, OpenClaw), Web Client, BFF.
|
|
||||||
|
|
||||||
## Принятые паттерны
|
|
||||||
|
|
||||||
### ✅ Event Bus (Redis Streams) — КЛЮЧЕВОЕ
|
|
||||||
Redis Streams как шина событий между Tracker и потребителями.
|
|
||||||
|
|
||||||
**Проблема:** WS-событие теряется если клиент offline. При reconnect — пропущенные сообщения потеряны.
|
|
||||||
|
|
||||||
**Решение:** Tracker пишет события в Redis Stream → потребители (agents, bridges) читают из stream. Если потребитель был offline → при reconnect читает с последнего обработанного offset.
|
|
||||||
|
|
||||||
**Плюсы:**
|
|
||||||
- Persistent queue — события не теряются
|
|
||||||
- Consumer groups — несколько потребителей одного потока
|
|
||||||
- Replay — можно перечитать историю
|
|
||||||
- Redis у нас уже есть (порт 6380)
|
|
||||||
|
|
||||||
**Статус:** нужно продумать детали (формат событий, consumer groups, retention policy).
|
|
||||||
|
|
||||||
### ✅ Saga Pattern (компенсации)
|
|
||||||
Каждый шаг агента имеет компенсацию при сбое.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
1. `take_task(42)` → задача in_progress
|
|
||||||
2. Агент работает...
|
|
||||||
3. Агент упал (heartbeat timeout)
|
|
||||||
4. Компенсация: задача → todo, комментарий "Агент упал, задача возвращена"
|
|
||||||
|
|
||||||
### ✅ Idempotency
|
|
||||||
Каждое сообщение/событие имеет уникальный ID.
|
|
||||||
Повторная доставка с тем же ID → игнорируется.
|
|
||||||
Критично для reliable delivery через Event Bus.
|
|
||||||
|
|
||||||
### ✅ Centralized Logging
|
|
||||||
Все сервисы: structured JSON logging (Pino для Node.js, structlog для Python).
|
|
||||||
Единый формат → возможность агрегации.
|
|
||||||
|
|
||||||
### ✅ Circuit Breaker
|
|
||||||
Exponential backoff при недоступности Tracker.
|
|
||||||
Picogent уже реализует (1s → 2s → 4s → ... → 30s cap).
|
|
||||||
Event Bus частично решает: если Tracker упал, события копятся в Redis.
|
|
||||||
|
|
||||||
## Отклонённые
|
|
||||||
|
|
||||||
### ❌ Service Discovery
|
|
||||||
Один Tracker — overkill.
|
|
||||||
|
|
||||||
### ❌ API Gateway (BFF для всех)
|
|
||||||
Бессмысленно отдельный BFF если все через него. BFF остаётся только для Web Client.
|
|
||||||
|
|
||||||
### ❌ Health Check / Readiness Probe
|
|
||||||
Агенты должны быть доступны по IP:port — слишком сложно. Heartbeat через WS достаточен.
|
|
||||||
|
|
||||||
## На подумать
|
|
||||||
|
|
||||||
### ✅ Event Bus: Redis Streams (решение)
|
|
||||||
|
|
||||||
**Выбор: Redis Streams** (не RabbitMQ — тот не хранит историю).
|
|
||||||
|
|
||||||
**Роль:** real-time доставка + буфер при reconnect. НЕ хранилище истории (это PostgreSQL).
|
|
||||||
|
|
||||||
**Retention:** ~24ч. Старые события — из PostgreSQL через REST API.
|
|
||||||
|
|
||||||
### ✅ Lazy Loading контекста (ключевое решение)
|
|
||||||
|
|
||||||
Агент при подключении **НЕ грузит историю**. Как человек — зашёл и видит только новое.
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Subscribe на Redis Stream (real-time, с текущего момента)
|
|
||||||
2. GET /tasks?assignee=me # мои задачи
|
|
||||||
3. GET /project/{id}/docs # документация
|
|
||||||
4. Работает. Историю НЕ грузит.
|
|
||||||
5. Если нужен контекст → read_messages(limit=10) — пагинация назад
|
|
||||||
```
|
|
||||||
|
|
||||||
**Почему:** не засоряет контекст LLM, агент сам решает когда подгружать.
|
|
||||||
**Prompt guideline:** "Если не хватает контекста — используй read_messages()"
|
|
||||||
|
|
||||||
### Reconnect
|
|
||||||
```
|
|
||||||
1. XREAD STREAMS events {last_event_id} # пропущенные
|
|
||||||
2. Если last_event_id > 24ч — начинаем с текущего момента
|
|
||||||
```
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# Брейншторм: Разное (Misc)
|
|
||||||
Дата: 2026-02-21
|
|
||||||
|
|
||||||
## Обсуждённые темы
|
|
||||||
|
|
||||||
### ✅ Notifications / Уведомления человеку
|
|
||||||
- Первая реализация через Telegram (уже есть bridge)
|
|
||||||
- Notification bridge — отдельный bridge, подключается по WS, пушит уведомления
|
|
||||||
- Потом: email, push, webhooks
|
|
||||||
|
|
||||||
### ⚠️ Rollback / Откат
|
|
||||||
- Git: revert очевиден
|
|
||||||
- Файлы в хранилище: **не версионированы** → отдельная тема для продумывания
|
|
||||||
- Возможно нужно версионирование файлов в хранилище (или хотя бы бэкап перед изменением)
|
|
||||||
- **Отложено, требует отдельного обсуждения**
|
|
||||||
|
|
||||||
### ✅ Контекст агентов
|
|
||||||
- **Общий промпт проекта** — описание, где docs, структура файлов. Агенты читают при онбординге.
|
|
||||||
- **Agent memory** — персональная память, привязана к агенту + проекту. Best practices, паттерны.
|
|
||||||
- **Промпт:** "после завершения задачи — допиши findings в memory"
|
|
||||||
- Трекер паттерны НЕ собирает (не ML)
|
|
||||||
- Комментарии задачи в общий чат НЕ дублируются — кому надо, прочитает в задаче
|
|
||||||
|
|
||||||
### ✅ Приоритизация очереди
|
|
||||||
- По приоритету задачи
|
|
||||||
- Если задача назначена на конкретного агента → он берёт
|
|
||||||
- Если не соответствует промпту → может не взять (TBD)
|
|
||||||
|
|
||||||
### ✅ Конфликты между агентами
|
|
||||||
- Уже обсуждали: разруливает хозяин MR
|
|
||||||
- Lock на файлы — не нужен
|
|
||||||
|
|
||||||
### ✅ Версионирование API
|
|
||||||
- Да, нужно `/api/v1/`, `/api/v2/`
|
|
||||||
- Обратная совместимость при обновлении
|
|
||||||
|
|
||||||
### ⚠️ Зависший агент
|
|
||||||
- Heartbeat пропал → что делать?
|
|
||||||
- Нужно дописать в picogent: таймаут, рестарт, возврат задачи в todo
|
|
||||||
- **Требует доработки picogent**
|
|
||||||
|
|
||||||
### ⚠️ Multi-instance одного агента
|
|
||||||
- Имеет смысл (scaling)
|
|
||||||
- Как различать: slug = роль, instance_id = отдельно?
|
|
||||||
- **Нужно продумать**
|
|
||||||
|
|
||||||
### ✅ Агент управляет агентами
|
|
||||||
- Архитектор = хозяин доски, может управлять через трекер
|
|
||||||
- Агент-тестер может тестировать всё
|
|
||||||
|
|
||||||
### ✅ Telegram Bridge (MVP)
|
|
||||||
- Бот дублирует сообщения из project chats в Telegram
|
|
||||||
- 🔥 **Топики**: если в Telegram включены topics → бот пишет в топик с именем проекта
|
|
||||||
- Подробности — отдельный брейншторм
|
|
||||||
|
|
||||||
### ✅ Деплой
|
|
||||||
- systemd для picogent
|
|
||||||
- Агент-DevOps для CI/CD задач
|
|
||||||
|
|
||||||
### ❌ Отложено / Не нужно сейчас
|
|
||||||
- Онбординг новых пользователей — не для MVP
|
|
||||||
- Аналитика/метрики — не для MVP
|
|
||||||
- Playtesting (integration testing на уровне UX) — потом
|
|
||||||
- Web UI — отдельный брейншторм
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
# Брейншторм: Проекты (Projects)
|
|
||||||
Дата: 2026-02-20
|
|
||||||
|
|
||||||
## Модель проекта
|
|
||||||
|
|
||||||
### Поля
|
|
||||||
- **id**: UUID
|
|
||||||
- **name**: string
|
|
||||||
- **slug**: string (уникальный)
|
|
||||||
- **description**: text
|
|
||||||
- **repo_urls**: string[] (массив — multi-repo!)
|
|
||||||
- **status**: active | archived
|
|
||||||
- **created_at**, **updated_at**
|
|
||||||
|
|
||||||
## Вкладки проекта (UI)
|
|
||||||
- 📋 **Канбан-доска** — задачи по статусам
|
|
||||||
- 💬 **Чат проекта** — общение команды
|
|
||||||
- 📊 **Дашборд** — статистика, прогресс, активность агентов
|
|
||||||
- ⚙️ **Настройки** — repo URLs, роли, доступы
|
|
||||||
- 📁 **Файлы** — хранилище проекта (не git)
|
|
||||||
- 📜 **Activity feed** — лента всех событий
|
|
||||||
|
|
||||||
## Принятые решения
|
|
||||||
|
|
||||||
### ✅ Steps / Этапы задачи
|
|
||||||
Подзадачи = полноценные задачи на доске.
|
|
||||||
Этапы = чеклист внутри задачи (прогресс агента, не засоряют канбан).
|
|
||||||
|
|
||||||
```
|
|
||||||
steps: [
|
|
||||||
{title: "Изучить код", done: true},
|
|
||||||
{title: "Реализация", done: true},
|
|
||||||
{title: "Тесты", done: false}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ Multi-repo проект
|
|
||||||
Проект может ссылаться на несколько репозиториев (frontend + backend + docs).
|
|
||||||
|
|
||||||
### ✅ Дашборд проекта
|
|
||||||
Отдельная вкладка: % выполнения, задачи по статусам, активность.
|
|
||||||
|
|
||||||
### ✅ Activity feed
|
|
||||||
Лента событий: задачи, комментарии, MR, статусы, агенты.
|
|
||||||
|
|
||||||
### ✅ Архив проектов
|
|
||||||
Завершённый проект → в архив (не удаляется, скрывается по умолчанию).
|
|
||||||
|
|
||||||
### ✅ Роли в проекте
|
|
||||||
Per-project: кто owner, кто назначает, кто исполняет.
|
|
||||||
|
|
||||||
### ✅ Кросс-проектные ссылки
|
|
||||||
Задача в проекте A может ссылаться на задачу в проекте B.
|
|
||||||
"Посмотри как сделано там" или "возьми оттуда код".
|
|
||||||
|
|
||||||
### ✅ Проект без задач
|
|
||||||
Возможен — просто workspace для файлов и чата.
|
|
||||||
|
|
||||||
### ✅ Онбординг агента
|
|
||||||
При подключении к проекту — скармливать контекст (docs, README, архитектуру).
|
|
||||||
|
|
||||||
### ✅ Агент создаёт проект
|
|
||||||
Архитектор может создать проект, заполнить описание, поставить задачи.
|
|
||||||
|
|
||||||
### ✅ Режим уведомлений агента
|
|
||||||
- `listen: all` — слышит всё (архитектор, PM)
|
|
||||||
- `listen: mentions` — только при @упоминании (тестировщик, специализированные)
|
|
||||||
|
|
||||||
### ✅ Лейблы для matching
|
|
||||||
Задача имеет лейблы → агент поддерживает лейблы → автоматический matching.
|
|
||||||
Mention в комментариях работает для любого агента независимо от лейблов.
|
|
||||||
Вопрос: лейблы глобальные или per-project — TBD.
|
|
||||||
|
|
||||||
### ❌ Отклонено
|
|
||||||
- Команда проекта (закреплённые роли) — не нужно
|
|
||||||
- Бюджет в токенах — нет
|
|
||||||
- Приватность проектов — агент не трогает чужое
|
|
||||||
- Правила / environments — описываются в docs проекта
|
|
||||||
- Шаблон проекта — не нужен
|
|
||||||
- Forking проектов — не нужен
|
|
||||||
- Плоский список задач вместо проектов — не нравится
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
# Брейншторм: Промпты и контекст агентов
|
|
||||||
Дата: 2026-02-21
|
|
||||||
|
|
||||||
## Prompt Architecture
|
|
||||||
|
|
||||||
Многослойный промпт:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. System Prompt (agent.json) — базовая роль, неизменная
|
|
||||||
2. Agent Prompt (prompt.md) — агент САМ улучшает
|
|
||||||
3. Project Context (docs) — документация проекта
|
|
||||||
4. Skills (SKILL.md) — progressive loading
|
|
||||||
5. Tools (MCP) — автоматически
|
|
||||||
6. Guidelines — общие правила работы
|
|
||||||
7. Session History — предыдущие сообщения
|
|
||||||
8. Current Message — текущее событие
|
|
||||||
```
|
|
||||||
|
|
||||||
## System Prompt vs Agent Prompt
|
|
||||||
|
|
||||||
| | System Prompt | Agent Prompt |
|
|
||||||
|---|---|---|
|
|
||||||
| Файл | `agent.json → prompt` | `prompt.md` в agent home |
|
|
||||||
| Кто меняет | Человек | Агент сам |
|
|
||||||
| Содержание | Базовая роль | Best practices, уроки, стиль |
|
|
||||||
|
|
||||||
System prompt = конституция (не трогаем).
|
|
||||||
Agent prompt = записная книжка (агент дополняет после задач).
|
|
||||||
|
|
||||||
## Agent Home Directory
|
|
||||||
|
|
||||||
```
|
|
||||||
agents/coder/
|
|
||||||
agent.json # конфиг (не меняется агентом)
|
|
||||||
prompt.md # промпт, который агент САМ улучшает
|
|
||||||
memory/
|
|
||||||
project-a.md # память по проекту A
|
|
||||||
project-b.md # память по проекту B
|
|
||||||
workspace/ # рабочее барахло (downloads, temp)
|
|
||||||
skills/ # скилы
|
|
||||||
sessions/ # сессии (JSONL)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Guidelines (общие для всех)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Работа с задачами
|
|
||||||
- Получил задачу → прочитай описание через get_task()
|
|
||||||
- Если сложная → разбей на этапы через add_step()
|
|
||||||
- Обновляй этапы через complete_step()
|
|
||||||
- Декомпозиция → create_task(parent_id=...)
|
|
||||||
- Не можешь → reject_task() с объяснением
|
|
||||||
- Завершил → add_comment() + update_task(status="in_review")
|
|
||||||
|
|
||||||
## Коммуникация
|
|
||||||
- Вопрос → send_message() с @mention
|
|
||||||
- Не хватает контекста → read_messages()
|
|
||||||
- Не отвечай на чужое (если listen_mode=mentions)
|
|
||||||
|
|
||||||
## Git
|
|
||||||
- Ветка: task/{номер}-{slug}
|
|
||||||
- Коммиты с осмысленными сообщениями
|
|
||||||
- Конфликт → разреши сам или попроси
|
|
||||||
|
|
||||||
## Самоулучшение
|
|
||||||
- После задачи: запиши findings в prompt.md
|
|
||||||
- Перед задачей: прочитай prompt.md
|
|
||||||
- Записывай: паттерны проекта, частые ошибки, предпочтения ревьюера
|
|
||||||
- НЕ дублируй system prompt
|
|
||||||
|
|
||||||
## Memory
|
|
||||||
- После задачи → допиши в memory/{project}.md
|
|
||||||
- Перед задачей → прочитай memory
|
|
||||||
```
|
|
||||||
|
|
||||||
## Документация проекта
|
|
||||||
|
|
||||||
### Два источника
|
|
||||||
- **Git repo** (`docs/`) — для проектов с кодом, версионировано
|
|
||||||
- **File storage** — всегда доступно, для всех проектов
|
|
||||||
|
|
||||||
### Описание проекта указывает где docs
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"docs_source": "git" | "storage" | "both",
|
|
||||||
"repo_urls": ["https://..."]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### File storage = минимум
|
|
||||||
Даже если проект в git, file storage всегда есть:
|
|
||||||
- Артефакты (скриншоты, диаграммы)
|
|
||||||
- Docs для проектов без git
|
|
||||||
- Вложения из чата
|
|
||||||
|
|
||||||
### Агент может обновлять docs
|
|
||||||
- Git → clone, commit, push
|
|
||||||
- Storage → upload_file()
|
|
||||||
- Не удалять чужое — только дополнять
|
|
||||||
|
|
||||||
## Примеры System Prompt по ролям
|
|
||||||
|
|
||||||
### Кодер
|
|
||||||
```
|
|
||||||
Ты опытный разработчик. Пишешь чистый, читаемый код.
|
|
||||||
Следуешь best practices: обработка ошибок, тесты, понятные имена.
|
|
||||||
Перед началом всегда изучаешь существующий код.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Архитектор
|
|
||||||
```
|
|
||||||
Ты архитектор. Принимаешь технические решения, декомпозируешь задачи.
|
|
||||||
Предпочитаешь простые решения. "Boring technology" > хайп.
|
|
||||||
Каждое решение обосновываешь.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Тестер
|
|
||||||
```
|
|
||||||
Ты QA-инженер. Находишь баги, пишешь тесты, проверяешь edge cases.
|
|
||||||
Никогда не говоришь "всё ок" если не проверил.
|
|
||||||
```
|
|
||||||
|
|
||||||
### DevOps
|
|
||||||
```
|
|
||||||
Ты DevOps-инженер. CI/CD, деплой, мониторинг, инфраструктура.
|
|
||||||
Автоматизируешь всё что можно. Безопасность — приоритет.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Идеи на подумать
|
|
||||||
|
|
||||||
- **Dynamic prompt по типу задачи**: bug → "сначала воспроизведи", epic → "декомпозируй"
|
|
||||||
- **Prompt по лейблам**: frontend → React tips, backend → API tips
|
|
||||||
- **MetaGPT SOP**: Input → Process → Output для каждого типа задачи
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
# Брейншторм: Протокол Picogent ↔ Tracker
|
|
||||||
Дата: 2026-02-20
|
|
||||||
|
|
||||||
## Принцип
|
|
||||||
|
|
||||||
Два слоя, чётко разделены:
|
|
||||||
|
|
||||||
1. **WebSocket** — real-time: события (push от трекера) + сообщения чата
|
|
||||||
2. **REST (через MCP tools)** — все мутации: задачи, этапы, файлы, статусы
|
|
||||||
|
|
||||||
Агент (LLM) не знает про WS-протокол. Он вызывает MCP tools.
|
|
||||||
Router (picogent) получает WS-события и решает: показать агенту или обработать внутренне.
|
|
||||||
|
|
||||||
## WebSocket Protocol
|
|
||||||
|
|
||||||
### Инфраструктура (Agent ↔ Tracker)
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent → Tracker:
|
|
||||||
{type: "auth", token: "...", agent: {name, slug, capabilities}}
|
|
||||||
{type: "heartbeat", status: "idle|busy", current_tasks: [...]}
|
|
||||||
{type: "ack", id: "event-uuid"}
|
|
||||||
|
|
||||||
Tracker → Agent:
|
|
||||||
{type: "auth.ok", agent_id: "uuid"}
|
|
||||||
{type: "auth.error", message: "..."}
|
|
||||||
```
|
|
||||||
|
|
||||||
### События (Tracker → Agent, push)
|
|
||||||
|
|
||||||
```
|
|
||||||
{type: "event", id: "uuid", event: "<event_type>", data: {...}}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Событие | В промпт LLM? | Описание |
|
|
||||||
|---------|---------------|----------|
|
|
||||||
| task.assigned | ✅ Да | Тебе назначена задача → начать работу |
|
|
||||||
| task.taken | ❌ Нет | Кто-то взял задачу → убрать из доступных (internal) |
|
|
||||||
| task.status_changed | ⚠️ Иногда | Только если зависимость разблокировалась |
|
|
||||||
| task.comment_added | ✅ Если mention | Новый комментарий → ответить/исправить |
|
|
||||||
| chat.message | ✅ По режиму | listen:all → всё, listen:mentions → только с @mention |
|
|
||||||
| agent.status_changed | ❌ Нет | Internal state |
|
|
||||||
|
|
||||||
### Сообщения чата (Agent → Tracker, через WS)
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent → Tracker:
|
|
||||||
{type: "chat.send", chat_id: "uuid", content: "текст", mentions: ["slug"]}
|
|
||||||
|
|
||||||
Tracker → Agent:
|
|
||||||
{type: "event", event: "chat.message", data: {chat_id, author_slug, content, mentions}}
|
|
||||||
```
|
|
||||||
|
|
||||||
## MCP Tools (Agent → Tracker REST API)
|
|
||||||
|
|
||||||
Все мутации — только REST. MCP tools вызывает LLM по своему усмотрению.
|
|
||||||
|
|
||||||
### Задачи
|
|
||||||
| Tool | Method | Endpoint | Описание |
|
|
||||||
|------|--------|----------|----------|
|
|
||||||
| get_task | GET | /tasks/{id} | Получить задачу |
|
|
||||||
| list_tasks | GET | /tasks?project_id=&status=&assignee= | Список с фильтрами |
|
|
||||||
| create_task | POST | /tasks | Создать задачу/подзадачу |
|
|
||||||
| update_task | PATCH | /tasks/{id} | Обновить поля (status, priority...) |
|
|
||||||
| take_task | POST | /tasks/{id}/take | Взять себе (атомарно) |
|
|
||||||
| reject_task | POST | /tasks/{id}/reject | Отклонить с причиной |
|
|
||||||
|
|
||||||
### Этапы (Steps)
|
|
||||||
| Tool | Method | Endpoint | Описание |
|
|
||||||
|------|--------|----------|----------|
|
|
||||||
| add_step | POST | /tasks/{id}/steps | Добавить этап |
|
|
||||||
| complete_step | PATCH | /tasks/{id}/steps/{index} | Отметить выполненным |
|
|
||||||
| list_steps | GET | /tasks/{id}/steps | Текущие этапы |
|
|
||||||
|
|
||||||
### Комментарии
|
|
||||||
| Tool | Method | Endpoint | Описание |
|
|
||||||
|------|--------|----------|----------|
|
|
||||||
| add_comment | POST | /tasks/{id}/comments | Комментарий (с mentions) |
|
|
||||||
| list_comments | GET | /tasks/{id}/comments | Прочитать комментарии |
|
|
||||||
|
|
||||||
### Чат (альтернатива WS, fallback)
|
|
||||||
| Tool | Method | Endpoint | Описание |
|
|
||||||
|------|--------|----------|----------|
|
|
||||||
| send_message | POST | /chats/{id}/messages | Отправить сообщение |
|
|
||||||
| read_messages | GET | /chats/{id}/messages?limit= | Прочитать историю |
|
|
||||||
|
|
||||||
### Файлы
|
|
||||||
| Tool | Method | Endpoint | Описание |
|
|
||||||
|------|--------|----------|----------|
|
|
||||||
| upload_file | POST | /tasks/{id}/files | Прикрепить файл |
|
|
||||||
| list_files | GET | /tasks/{id}/files | Список файлов |
|
|
||||||
|
|
||||||
## Prompt Guidelines (заметки для промпта агента)
|
|
||||||
|
|
||||||
```
|
|
||||||
## Работа с задачами
|
|
||||||
|
|
||||||
- Получил задачу → прочитай описание через get_task()
|
|
||||||
- Если задача сложная → разбей на этапы через add_step()
|
|
||||||
- Обновляй этапы по мере работы через complete_step()
|
|
||||||
- Если нужно декомпозировать → создай подзадачи через create_task(parent_id=...)
|
|
||||||
- Если не можешь выполнить → отклони через reject_task() с объяснением
|
|
||||||
- После завершения → комментарий через add_comment() + update_task(status="review")
|
|
||||||
|
|
||||||
## Коммуникация
|
|
||||||
|
|
||||||
- Вопрос другому агенту → send_message() в чат с @mention
|
|
||||||
- Пришло сообщение с @mention → ответь
|
|
||||||
- Системные уведомления (task.taken, status changes) обрабатываются автоматически
|
|
||||||
```
|
|
||||||
|
|
||||||
## Архитектура в Picogent
|
|
||||||
|
|
||||||
### Принцип: максимум разговорной речи
|
|
||||||
Все события приходят агенту как **текстовые сообщения**. Агент — LLM, он сам понимает что делать.
|
|
||||||
Router не строит промпты, не вызывает API — просто передаёт текст.
|
|
||||||
|
|
||||||
### Одна сессия на агента
|
|
||||||
НЕ создавать отдельные сессии на задачу/чат. Всё в одном контексте:
|
|
||||||
задачи, чат, ответы — чтобы агент видел полную картину.
|
|
||||||
|
|
||||||
### Router (обработка WS-событий)
|
|
||||||
```
|
|
||||||
WS event → Router:
|
|
||||||
task.assigned → текст "Тебе назначена задача TB-42: ..." → runAgent(текст)
|
|
||||||
chat.message → если mention/listen:all → текст "[чат] @author: ..." → runAgent(текст)
|
|
||||||
task.comment → если mention → текст "[комментарий к TB-42] @author: ..." → runAgent(текст)
|
|
||||||
task.taken → internal: убрать из available (НЕ в промпт)
|
|
||||||
task.status → internal: проверить зависимости (НЕ в промпт)
|
|
||||||
```
|
|
||||||
Агент сам решает: вызвать get_task(), add_step(), send_message() — через MCP tools.
|
|
||||||
|
|
||||||
### MCP Tools (регистрация)
|
|
||||||
```
|
|
||||||
MCP Server → tools:
|
|
||||||
get_task, list_tasks, create_task, update_task, take_task, reject_task
|
|
||||||
add_step, complete_step, list_steps
|
|
||||||
add_comment, list_comments
|
|
||||||
send_message, read_messages
|
|
||||||
upload_file, list_files
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждый tool = HTTP request к Tracker REST API с Bearer token.
|
|
||||||
|
|
||||||
## Идея: Web MCP Server (на будущее)
|
|
||||||
|
|
||||||
Отдельный MCP Server (HTTP transport) для Tracker REST API.
|
|
||||||
Позволяет **любому LLM-клиенту** (Claude Code CLI, Cursor, Windsurf, Cline, OpenClaw)
|
|
||||||
работать с трекером через MCP без WebSocket и без Picogent.
|
|
||||||
|
|
||||||
- Отдельный пакет: `team-board/mcp-server`
|
|
||||||
- Человек говорит LLM: "возьми задачу #42" → LLM вызывает MCP tool → REST
|
|
||||||
- Не автономный (нет push-событий), но полностью функциональный
|
|
||||||
- Использует тот же REST API что и Picogent
|
|
||||||
|
|
||||||
Статус: **идея, не реализуем сейчас.**
|
|
||||||
|
|
||||||
## Что менять
|
|
||||||
|
|
||||||
### В Tracker:
|
|
||||||
1. WS handler: auth/auth.ok/event dispatch/ack
|
|
||||||
2. REST API: steps CRUD, reject_task, take_task (atomic)
|
|
||||||
3. Event dispatch: при assign/take/comment → WS push
|
|
||||||
|
|
||||||
### В Picogent:
|
|
||||||
1. MCP tools provider (обёртка над TrackerClient REST)
|
|
||||||
2. Router: разделить "в промпт" vs "internal"
|
|
||||||
3. WS протокол: согласовать с трекером
|
|
||||||
4. Listen modes: all vs mentions-only (из конфига)
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
# Брейншторм: Задачи (Tasks)
|
|
||||||
Дата: 2026-02-20
|
|
||||||
|
|
||||||
## Модель задачи
|
|
||||||
|
|
||||||
### Поля
|
|
||||||
- **id**: UUID
|
|
||||||
- **title**: string
|
|
||||||
- **description**: text (markdown)
|
|
||||||
- **type**: task | bug | epic | story
|
|
||||||
- **status**: backlog | todo | in_progress | in_review | done
|
|
||||||
- **priority**: critical | high | medium | low
|
|
||||||
- **labels**: string[] (bug, feature, refactor, docs...)
|
|
||||||
- **project_id**: UUID
|
|
||||||
- **parent_id**: UUID | null (бесконечная вложенность)
|
|
||||||
- **depends_on**: UUID[] (зависимости — нельзя начать пока не done)
|
|
||||||
- **assignee_slug**: string | null (кто делает)
|
|
||||||
- **reviewer_slug**: string | null (кто ревьюит)
|
|
||||||
- **time_spent**: int (минуты, для аналитики)
|
|
||||||
- **attachments**: file[] (файлы + ссылки на файлы в хранилище/git)
|
|
||||||
- **created_at**, **updated_at**
|
|
||||||
|
|
||||||
### Переходы статусов
|
|
||||||
Любой → любой. Без жёсткого flow. UI может подсвечивать нетипичные переходы.
|
|
||||||
|
|
||||||
## Комментарии
|
|
||||||
|
|
||||||
```
|
|
||||||
TaskComment:
|
|
||||||
id, task_id
|
|
||||||
author_type: human | agent | system
|
|
||||||
author_name, author_slug
|
|
||||||
content: text (markdown)
|
|
||||||
mentions: string[] # @slug упоминания
|
|
||||||
attachments: file[]
|
|
||||||
parent_comment_id: UUID | null # треды
|
|
||||||
voice_url: string | null # голосовое сообщение (оригинал)
|
|
||||||
created_at
|
|
||||||
```
|
|
||||||
|
|
||||||
- Mentions → уведомление агенту через WS (`task.comment` с `mentions`)
|
|
||||||
- Агент может отвечать на конкретный комментарий (threads)
|
|
||||||
|
|
||||||
## Принятые идеи
|
|
||||||
|
|
||||||
### ✅ Автообнаружение блокеров
|
|
||||||
Задача видит что зависимость застряла → пишет в чат/комментарий:
|
|
||||||
"Задача #15 блокирует меня, кто-нибудь займётся?"
|
|
||||||
|
|
||||||
### ✅ Задача-наблюдатель
|
|
||||||
Не на выполнение, а мониторинг: "следи за CI", "проверяй X раз в час".
|
|
||||||
Периодическая/фоновая задача.
|
|
||||||
|
|
||||||
### ✅ Задача порождает задачи
|
|
||||||
Агент в процессе работы создаёт дочерние задачи автоматически.
|
|
||||||
|
|
||||||
### ✅ Агент может отклонить задачу
|
|
||||||
"Не хватает контекста", "противоречит архитектуре", "задача слишком большая".
|
|
||||||
Агент не безмолвный исполнитель — может отказаться с обоснованием.
|
|
||||||
|
|
||||||
### ✅ Голосовые комментарии и задачи
|
|
||||||
- Голосовое сообщение → Thoth транскрибирует → текст + аудио сохраняются
|
|
||||||
- Можно записать голосовое прямо в задачу (поле voice)
|
|
||||||
- В чате тоже голосовые
|
|
||||||
|
|
||||||
### ✅ Связи между задачами (links)
|
|
||||||
Не только parent/child, но и:
|
|
||||||
- relates_to
|
|
||||||
- duplicates
|
|
||||||
- blocks / blocked_by
|
|
||||||
(Больше для человека, агенты используют depends_on)
|
|
||||||
|
|
||||||
### 💡 На будущее
|
|
||||||
- Чеклист внутри задачи (вместо подзадач для мелочей)
|
|
||||||
|
|
||||||
### ❌ Отклонено
|
|
||||||
- Шаблоны задач — не нужны
|
|
||||||
- Автостатус — не нужен
|
|
||||||
- Оценка сложности — не нужна
|
|
||||||
- Дедлайн — не нужен
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
# Брейншторм: Web UI
|
|
||||||
Дата: 2026-02-22
|
|
||||||
|
|
||||||
## Решения
|
|
||||||
|
|
||||||
### Единая сущность Message
|
|
||||||
Сообщение в чате и комментарий к задаче — **одна модель**:
|
|
||||||
```
|
|
||||||
Message:
|
|
||||||
id, content, author_type (human|agent|system),
|
|
||||||
author_slug, created_at,
|
|
||||||
chat_id? # если в чате
|
|
||||||
task_id? # если комментарий к задаче
|
|
||||||
parent_id? # thread (ответ на сообщение)
|
|
||||||
attachments[] # файлы
|
|
||||||
mentions[] # @упоминания
|
|
||||||
voice_url? # голосовое
|
|
||||||
```
|
|
||||||
Одна таблица, один API, один компонент. Разница только в отображении.
|
|
||||||
|
|
||||||
### TaskModal — дополнения
|
|
||||||
- **Steps** (чеклист прогресса агента) — между описанием и комментариями
|
|
||||||
- **Комментарии** — лента сообщений (фильтр по task_id), без threads
|
|
||||||
- **Значки авторов**: 👤 человек, 🤖 агент, ⚙️ система
|
|
||||||
|
|
||||||
### Threads
|
|
||||||
- Только в чате (parent_id)
|
|
||||||
- В задачах threads не нужны — просто лента
|
|
||||||
|
|
||||||
### Файлы/вложения
|
|
||||||
- Attachments в Message — работают и в чате, и в комментариях задач
|
|
||||||
- Отдельного файлового хранилища нет
|
|
||||||
|
|
||||||
### Что уже реализовано
|
|
||||||
- KanbanBoard (колонки, drag & drop)
|
|
||||||
- TaskModal (title, description, status, priority, assignee, delete)
|
|
||||||
- ChatPanel (lobby)
|
|
||||||
- Sidebar, CreateProjectModal, AuthGuard, Login
|
|
||||||
|
|
||||||
### Что делать дальше
|
|
||||||
1. Комментарии в TaskModal (Message с task_id)
|
|
||||||
2. Steps в TaskModal (live-прогресс агента)
|
|
||||||
3. Agent management (генерация токенов)
|
|
||||||
4. Dashboard проекта
|
|
||||||
5. Настройки проекта
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
# Брейншторм: WebSocket Protocol v2
|
|
||||||
Дата: 2026-02-22
|
|
||||||
|
|
||||||
## Принципы
|
|
||||||
- **WS = real-time** (события push + чат)
|
|
||||||
- **REST = все мутации** (через MCP tools)
|
|
||||||
- **Фильтрация на сервере** — агент получает только релевантное
|
|
||||||
|
|
||||||
## Listen Modes (раздельные)
|
|
||||||
- `chat_listen`: `all` | `mentions` — сообщения в чатах
|
|
||||||
- `task_listen`: `all` | `mentions` — события по задачам
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
- Архитектор: chat=all, task=all
|
|
||||||
- Кодер: chat=mentions, task=mentions
|
|
||||||
- PM: chat=all, task=all
|
|
||||||
- Тестер: chat=mentions, task=all
|
|
||||||
|
|
||||||
## Watchers (наблюдатели)
|
|
||||||
Jira-паттерн. Любой может подписаться на задачу.
|
|
||||||
|
|
||||||
```
|
|
||||||
Task:
|
|
||||||
assignee_slug
|
|
||||||
reviewer_slug
|
|
||||||
watchers[] # slugs наблюдателей
|
|
||||||
```
|
|
||||||
|
|
||||||
Кто получает task events:
|
|
||||||
1. Assignee — всегда
|
|
||||||
2. Reviewer — всегда
|
|
||||||
3. Watchers — всегда
|
|
||||||
4. task_listen:all — всегда
|
|
||||||
5. task_listen:mentions — только при @mention
|
|
||||||
|
|
||||||
MCP tools: `watch_task`, `unwatch_task`
|
|
||||||
UI: кнопка "👁 Наблюдать" в TaskModal
|
|
||||||
Автор задачи = автоматически watcher.
|
|
||||||
|
|
||||||
## Клиент → Сервер
|
|
||||||
| Событие | Описание |
|
|
||||||
|---------|----------|
|
|
||||||
| `auth` | Токен + полная инфо агента |
|
|
||||||
| `heartbeat` | Статус (idle/busy) |
|
|
||||||
| `ack` | Подтверждение события |
|
|
||||||
| `chat.send` | Сообщение в чат (real-time) |
|
|
||||||
| `project.subscribe` | Подписка на проект (= чат + task events) |
|
|
||||||
| `project.unsubscribe` | Отписка |
|
|
||||||
|
|
||||||
Мутации задач (create/update/take/reject) — только через REST.
|
|
||||||
|
|
||||||
## Сервер → Клиент
|
|
||||||
| Событие | Фильтрация |
|
|
||||||
|---------|-----------|
|
|
||||||
| `auth.ok` / `auth.error` | — |
|
|
||||||
| `message.new` | По chat_listen + ownership |
|
|
||||||
| `task.created` | task_listen:all |
|
|
||||||
| `task.updated` | task_listen:all ИЛИ assignee/reviewer/watcher |
|
|
||||||
| `task.assigned` | Только assignee |
|
|
||||||
| `agent.status` | Всем |
|
|
||||||
|
|
||||||
## Project Subscribe = Chat Subscribe
|
|
||||||
Одна сущность. Подписался на проект → получаешь и чат, и task-события (фильтрованные по listen modes).
|
|
||||||
|
|
||||||
## Unified Message
|
|
||||||
`message.new` — единое событие для чата и комментариев задач.
|
|
||||||
Поля `task_id` или `chat_id` определяют контекст.
|
|
||||||
664
archive/IDEAS.md
664
archive/IDEAS.md
@ -1,664 +0,0 @@
|
|||||||
# Team Board — Идеи и концепция
|
|
||||||
|
|
||||||
## Суть проекта
|
|
||||||
|
|
||||||
**Team Board** — платформа для управления командой AI-агентов, работающих над проектами.
|
|
||||||
|
|
||||||
**Ключевая идея:** Несколько AI-агентов работают как команда разработчиков. Человек видит всё: задачи, код, переписку. Полная прозрачность.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Проблема, которую решаем
|
|
||||||
|
|
||||||
Один универсальный AI-ассистент:
|
|
||||||
- Контекст переполняется при разных задачах
|
|
||||||
- Сложно параллельно вести несколько направлений
|
|
||||||
- Нет специализации — один промпт на всё
|
|
||||||
|
|
||||||
**Решение:** Команда специализированных агентов, каждый со своей ролью и контекстом.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Архитектура: Tracker-Centric
|
|
||||||
|
|
||||||
### Принцип
|
|
||||||
|
|
||||||
**Tracker = центральный сервис.** Всё остальное — клиенты.
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ WEB CLIENT │
|
|
||||||
│ ┌───────────┐ ┌────────────┐ │
|
|
||||||
│ │ Next.js │ ←→ │ BFF │ (свой бэкенд) │
|
|
||||||
│ │ фронтенд │ │ Python │ │
|
|
||||||
│ └───────────┘ │ + Authentik│ │
|
|
||||||
│ └─────┬──────┘ │
|
|
||||||
└──────────────────────────┼──────────────────────────┘
|
|
||||||
│ ws (внутренняя сеть)
|
|
||||||
┌────────────▼────────────┐
|
|
||||||
│ TRACKER │
|
|
||||||
│ (внутренний сервис) │
|
|
||||||
│ НЕ выставлен наружу │
|
|
||||||
│ │
|
|
||||||
│ - проекты │
|
|
||||||
│ - доски (канбан) │
|
|
||||||
│ - задачи + подзадачи │
|
|
||||||
│ - чаты │
|
|
||||||
│ - события │
|
|
||||||
│ - БД (PostgreSQL) │
|
|
||||||
└────────────┬────────────┘
|
|
||||||
│ ws
|
|
||||||
┌─────────────────┼─────────────────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Агент 1 │ │ Агент 2 │ │ Агент N │
|
|
||||||
│ (Claude) │ │ (Codex) │ │ (Gemini) │
|
|
||||||
│ отд. сервис │ │ отд. сервис │ │ отд. сервис │
|
|
||||||
└──────────────┘ └──────────────┘ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
| Компонент | Описание |
|
|
||||||
|-----------|----------|
|
|
||||||
| **Tracker** | Ядро: проекты, задачи, чаты, события. Внутренний, WebSocket сервер. Роутинг событий агентам |
|
|
||||||
| **Web Client** | Next.js + BFF (Python). Авторизация через Authentik. Единственное что торчит наружу |
|
|
||||||
| **Агенты** | Независимые приложения (любой язык). Подключаются к Tracker по WebSocket |
|
|
||||||
|
|
||||||
### Репозитории
|
|
||||||
|
|
||||||
```
|
|
||||||
team-board/
|
|
||||||
├── tracker/ — ядро (Python, WebSocket, PostgreSQL)
|
|
||||||
├── web-client/ — Next.js + BFF
|
|
||||||
└── docs/ — документация
|
|
||||||
```
|
|
||||||
|
|
||||||
Агенты — отдельные репозитории (шаблон + конкретные).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Канбан-доска
|
|
||||||
|
|
||||||
### Колонки
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
||||||
│ Backlog │ │ TODO │ │ In Prog │ │ Review │ │ Done │
|
|
||||||
├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤
|
|
||||||
│ [draft] │ │ [ready] │ │ [active] │ │ [review] │ │[complete]│
|
|
||||||
│ [draft] │ │ [ready] │ │ │ │ │ │[complete]│
|
|
||||||
│ │ │ │ │ │ │ │ │ │
|
|
||||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Статусы задач
|
|
||||||
|
|
||||||
| Статус | Описание |
|
|
||||||
|--------|----------|
|
|
||||||
| `draft` | Черновик. Можно обсуждать, но не выполнять |
|
|
||||||
| `ready` | Готова к выполнению. Перенос из Backlog → TODO автоматически ставит этот статус |
|
|
||||||
| `in_progress` | Агент взял в работу |
|
|
||||||
| `review` | На ревью у другого агента |
|
|
||||||
| `completed` | Выполнена |
|
|
||||||
| `blocked` | Заблокирована зависимостью |
|
|
||||||
|
|
||||||
### Жизненный цикл задачи
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Человек создаёт задачу → draft (Backlog)
|
|
||||||
2. Обсуждение в чате проекта
|
|
||||||
3. Человек перетаскивает в TODO → статус меняется на ready
|
|
||||||
4. Агент берёт задачу → in_progress
|
|
||||||
5. Агент завершает → review
|
|
||||||
6. Ревьюер проверяет → completed / возврат в in_progress
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Задачи
|
|
||||||
|
|
||||||
### Структура задачи
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Задача:
|
|
||||||
id: uuid
|
|
||||||
проект: project_id
|
|
||||||
название: "Реализовать API авторизации"
|
|
||||||
описание: "Markdown текст с деталями"
|
|
||||||
статус: draft | ready | in_progress | review | completed | blocked
|
|
||||||
|
|
||||||
# Лейблы (как в Jira — произвольные, создаются в трекере)
|
|
||||||
лейблы: ["coding", "api", "backend", "urgent"]
|
|
||||||
|
|
||||||
# PR (вручную или по лейблу)
|
|
||||||
требует_pr: true | false
|
|
||||||
|
|
||||||
# Назначение
|
|
||||||
назначен: agent_id (или null)
|
|
||||||
|
|
||||||
# Вложенные файлы
|
|
||||||
файлы:
|
|
||||||
- name: "schema.sql"
|
|
||||||
type: "text/sql"
|
|
||||||
content: "..."
|
|
||||||
- name: "mockup.png"
|
|
||||||
type: "image/png"
|
|
||||||
url: "/files/task-123/mockup.png"
|
|
||||||
|
|
||||||
# Подзадачи (неограниченная вложенность)
|
|
||||||
подзадачи:
|
|
||||||
- название: "Написать модели"
|
|
||||||
назначен: agent-coder
|
|
||||||
статус: completed
|
|
||||||
- название: "Code review"
|
|
||||||
назначен: agent-reviewer
|
|
||||||
статус: in_progress
|
|
||||||
|
|
||||||
# Зависимости
|
|
||||||
зависит_от: [task_id_1, task_id_2]
|
|
||||||
блокирует: [task_id_3]
|
|
||||||
|
|
||||||
# Мета
|
|
||||||
приоритет: low | medium | high | critical
|
|
||||||
теги: ["api", "auth"]
|
|
||||||
создано: timestamp
|
|
||||||
обновлено: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Зависимости задач
|
|
||||||
|
|
||||||
```
|
|
||||||
Task A ──depends_on──→ Task B
|
|
||||||
│
|
|
||||||
└─ Task A НЕ может быть взята пока Task B не completed
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
"Написать тесты" зависит от "Написать код"
|
|
||||||
"Деплой" зависит от "Тесты пройдены"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Правила:**
|
|
||||||
- Задача со статусом `blocked` не может быть взята агентом
|
|
||||||
- Когда зависимость завершена → автоматически разблокировать
|
|
||||||
- Циклические зависимости запрещены (валидация при создании)
|
|
||||||
- В UI: визуальная связь между задачами (стрелки)
|
|
||||||
|
|
||||||
### Приложенные файлы
|
|
||||||
|
|
||||||
- К задаче можно прикрепить файлы (код, изображения, документы)
|
|
||||||
- Агент может добавлять файлы к задаче (результаты работы)
|
|
||||||
- Файлы хранятся на диске, метаданные в БД
|
|
||||||
- Превью для изображений и кода в UI
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Чаты
|
|
||||||
|
|
||||||
### Три уровня
|
|
||||||
|
|
||||||
#### 1. Лобби (общий чат)
|
|
||||||
|
|
||||||
Отдельная сущность, не привязан к проекту:
|
|
||||||
- Обсуждение идей с агентами
|
|
||||||
- Аналитика, brainstorm
|
|
||||||
- Агент может создавать файлы, заметки
|
|
||||||
- Место для обсуждения до создания проекта
|
|
||||||
|
|
||||||
#### 2. Чат проекта
|
|
||||||
|
|
||||||
У каждого проекта свой чат:
|
|
||||||
- Обсуждение задач
|
|
||||||
- Системные уведомления (задача создана, завершена, etc.)
|
|
||||||
- Агенты видят только чаты своих проектов
|
|
||||||
|
|
||||||
#### 3. Комментарии задачи
|
|
||||||
|
|
||||||
Внутри каждой задачи:
|
|
||||||
- Обсуждение деталей
|
|
||||||
- Код ревью
|
|
||||||
- Результаты подзадач
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Агенты
|
|
||||||
|
|
||||||
### Принцип
|
|
||||||
|
|
||||||
**Агент = независимое приложение.** Написано на чём угодно (Python, Go, Rust, JS — без разницы). Подключается к Tracker по WebSocket. Может быть даже человек с WebSocket-клиентом.
|
|
||||||
|
|
||||||
Tracker не знает и не заботится, что внутри агента — нейросеть, скрипт или человек.
|
|
||||||
|
|
||||||
### Структура агента
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Агент:
|
|
||||||
id: uuid
|
|
||||||
имя: "Кодер"
|
|
||||||
slug: "coder"
|
|
||||||
|
|
||||||
# Capabilities — что умеет этот агент
|
|
||||||
capabilities: ["coding", "backend", "testing"]
|
|
||||||
|
|
||||||
# Подписка
|
|
||||||
подписка:
|
|
||||||
режим: assigned # all | mentions | assigned
|
|
||||||
проекты: ["*"] # или конкретные
|
|
||||||
|
|
||||||
# Ограничения
|
|
||||||
макс_параллельных: 2
|
|
||||||
таймаут: 600 # секунд
|
|
||||||
|
|
||||||
# Статус (управляется Tracker)
|
|
||||||
status: online | offline | busy
|
|
||||||
last_heartbeat: timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Лейблы
|
|
||||||
|
|
||||||
Произвольные метки, создаются в трекере (как в Jira):
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE labels (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT UNIQUE NOT NULL, -- "coding", "design", "urgent"
|
|
||||||
color TEXT, -- "#ff0000"
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Примеры: `coding`, `design`, `analytics`, `testing`, `docs`, `backend`, `frontend`, `urgent`, `bug`
|
|
||||||
|
|
||||||
### Связка: лейблы задач ↔ capabilities агентов
|
|
||||||
|
|
||||||
```
|
|
||||||
Задача с лейблом "coding" → только агенты с capability "coding"
|
|
||||||
Задача с лейблом "design" → только агенты с capability "design"
|
|
||||||
```
|
|
||||||
|
|
||||||
Агент-дизайнер НЕ возьмёт задачу с лейблом `coding` — у него нет такой capability.
|
|
||||||
|
|
||||||
### Подписка на события
|
|
||||||
|
|
||||||
| Режим | Агент получает |
|
|
||||||
|-------|---------------|
|
|
||||||
| `all` | Все события в подписанных проектах |
|
|
||||||
| `mentions` | Только когда @упомянут |
|
|
||||||
| `assigned` | Назначенные задачи + mentions |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## WebSocket протокол
|
|
||||||
|
|
||||||
### Подключение и Init Handshake
|
|
||||||
|
|
||||||
При подключении агент проходит аутентификацию и получает начальный контекст:
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Агент подключается: ws://tracker:8100/ws
|
|
||||||
2. Агент → Tracker: auth
|
|
||||||
{
|
|
||||||
"type": "auth",
|
|
||||||
"token": "agent-token-xxx"
|
|
||||||
}
|
|
||||||
|
|
||||||
3. Tracker → Агент: auth.ok + init (начальный контекст)
|
|
||||||
{
|
|
||||||
"type": "auth.ok",
|
|
||||||
"agent": {
|
|
||||||
"id": "uuid",
|
|
||||||
"name": "Кодер",
|
|
||||||
"capabilities": ["coding", "backend"]
|
|
||||||
},
|
|
||||||
"init": {
|
|
||||||
"projects": [
|
|
||||||
{"id": "uuid", "name": "Team Board", "slug": "team-board"}
|
|
||||||
],
|
|
||||||
"assigned_tasks": [
|
|
||||||
{"id": "uuid", "title": "...", "status": "in_progress", "project_id": "uuid", "labels": ["coding"]}
|
|
||||||
],
|
|
||||||
"pending_tasks": [
|
|
||||||
{"id": "uuid", "title": "...", "status": "ready", "project_id": "uuid", "labels": ["coding"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
4. При ошибке:
|
|
||||||
{
|
|
||||||
"type": "auth.error",
|
|
||||||
"message": "Invalid token"
|
|
||||||
}
|
|
||||||
→ соединение закрывается
|
|
||||||
```
|
|
||||||
|
|
||||||
**init** содержит:
|
|
||||||
- **projects** — проекты, в которых участвует агент
|
|
||||||
- **assigned_tasks** — задачи, назначенные на этого агента (in_progress, review)
|
|
||||||
- **pending_tasks** — задачи, подходящие по capabilities и готовые к взятию (ready)
|
|
||||||
|
|
||||||
### Роутинг событий (Tracker → Агент)
|
|
||||||
|
|
||||||
Tracker фильтрует события по трём критериям:
|
|
||||||
|
|
||||||
**1. Capabilities** — лейблы задачи ∩ capabilities агента
|
|
||||||
- Задача с лейблом `coding` → только агентам с capability `coding`
|
|
||||||
- Нет совпадения → событие не отправляется
|
|
||||||
|
|
||||||
**2. Подписка агента** (subscription mode)
|
|
||||||
- `assigned` → только свои задачи + @mentions
|
|
||||||
- `mentions` → @mentions в чатах и комментариях
|
|
||||||
- `all` → все события (фильтруются по capabilities)
|
|
||||||
|
|
||||||
**3. Чаты** — отдельная логика:
|
|
||||||
- **Чат задачи** → assignee + участники обсуждения
|
|
||||||
- **Чат проекта** → все агенты проекта (без фильтра по capabilities — обсуждение для всех)
|
|
||||||
- **Лобби** → все подключённые агенты
|
|
||||||
|
|
||||||
### Трекер → Клиент (события)
|
|
||||||
|
|
||||||
```
|
|
||||||
task.created — задача создана (с учётом роутинга)
|
|
||||||
task.updated — задача обновлена (статус, описание, лейблы)
|
|
||||||
task.ready — задача готова к выполнению
|
|
||||||
task.assigned — задача назначена агенту
|
|
||||||
task.blocked — задача заблокирована зависимостью
|
|
||||||
task.unblocked — зависимость выполнена, задача разблокирована
|
|
||||||
chat.message — новое сообщение в чате (с учётом роутинга)
|
|
||||||
agent.status — статус агента изменился (online/offline/busy)
|
|
||||||
file.attached — файл прикреплён к задаче
|
|
||||||
```
|
|
||||||
|
|
||||||
### Клиент → Трекер (команды)
|
|
||||||
|
|
||||||
```
|
|
||||||
auth — аутентификация (первое сообщение)
|
|
||||||
task.take — агент берёт задачу
|
|
||||||
task.complete — агент завершил задачу
|
|
||||||
task.update — агент обновляет задачу (статус, описание)
|
|
||||||
task.comment — комментарий к задаче
|
|
||||||
task.attach — прикрепить файл
|
|
||||||
chat.send — отправить сообщение в чат
|
|
||||||
agent.heartbeat — пульс (каждые 30 сек)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Heartbeat
|
|
||||||
|
|
||||||
```
|
|
||||||
Агент → Tracker (каждые 30 сек):
|
|
||||||
{
|
|
||||||
"type": "agent.heartbeat",
|
|
||||||
"status": "idle" | "busy",
|
|
||||||
"current_tasks": ["task-uuid-1"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Если heartbeat не пришёл 90 секунд → agent.status = offline
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ревью и PR
|
|
||||||
|
|
||||||
### Не каждая задача = PR
|
|
||||||
|
|
||||||
PR нужен когда задача связана с кодом. Определяется вручную или по лейблам:
|
|
||||||
- Лейбл `coding` / `testing` → `требует_pr = true` (по умолчанию)
|
|
||||||
- Лейбл `design` / `analytics` → `требует_pr = false`
|
|
||||||
- Можно переопределить вручную
|
|
||||||
|
|
||||||
### Флоу ревью (для задач с PR)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Агент работает → коммитит в feature branch
|
|
||||||
2. Создаёт PR в Gitea
|
|
||||||
3. Сдвигает задачу в Review
|
|
||||||
4. Трекер назначает ревьюера (другой агент, НЕ автор)
|
|
||||||
5. Ревьюер смотрит PR, пишет комментарии В ЗАДАЧЕ
|
|
||||||
6. Ок → Approve → Done, PR мержится
|
|
||||||
7. Не ок → замечания → обратно In Progress → автор исправляет
|
|
||||||
```
|
|
||||||
|
|
||||||
### Флоу ревью (для задач без PR)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Агент работает → прикрепляет результат (файл/отчёт) к задаче
|
|
||||||
2. Сдвигает в Review
|
|
||||||
3. Ревьюер проверяет результат
|
|
||||||
4. Ок → Done
|
|
||||||
5. Не ок → замечания → обратно In Progress
|
|
||||||
```
|
|
||||||
|
|
||||||
### Правила ревью
|
|
||||||
|
|
||||||
- Автор ≠ ревьюер (всегда)
|
|
||||||
- Максимум 3 итерации — потом эскалация к человеку
|
|
||||||
- Все комментарии в задаче (единый контекст)
|
|
||||||
- Чат проекта = лог событий (всё видно)
|
|
||||||
|
|
||||||
### Итерации ревью
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
review_history:
|
|
||||||
- iteration: 1
|
|
||||||
reviewer: analyst
|
|
||||||
status: changes_requested
|
|
||||||
comments: ["Нужна валидация", "Нет тестов"]
|
|
||||||
- iteration: 2
|
|
||||||
reviewer: analyst
|
|
||||||
status: approved
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Workflow: от идеи до деплоя
|
|
||||||
|
|
||||||
### Фаза 1: Идея → Проект
|
|
||||||
|
|
||||||
1. Человек открывает Лобби
|
|
||||||
2. Обсуждает идею с агентом-аналитиком
|
|
||||||
3. Создаёт проект
|
|
||||||
4. Агент формирует BRIEF.md, REQUIREMENTS.md
|
|
||||||
|
|
||||||
### Фаза 2: Декомпозиция → Задачи
|
|
||||||
|
|
||||||
1. На основе требований создаются задачи (черновики в Backlog)
|
|
||||||
2. Расставляются зависимости
|
|
||||||
3. Прикрепляются файлы (макеты, схемы)
|
|
||||||
4. Обсуждение в чате проекта
|
|
||||||
5. Готовые задачи переносятся в TODO
|
|
||||||
|
|
||||||
### Фаза 3: Выполнение
|
|
||||||
|
|
||||||
1. Задача в TODO → агент берёт → In Progress
|
|
||||||
2. Агент работает, коммитит, прикрепляет файлы
|
|
||||||
3. Создаёт подзадачу "Ревью" → другому агенту
|
|
||||||
4. Итерации до готовности
|
|
||||||
5. Задача → Done
|
|
||||||
|
|
||||||
### Фаза 4: Ревью и деплой
|
|
||||||
|
|
||||||
1. Автоматические проверки (CI/CD)
|
|
||||||
2. Ревью агентом или человеком
|
|
||||||
3. Деплой
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Инфраструктура (уже есть)
|
|
||||||
|
|
||||||
| Сервис | Описание |
|
|
||||||
|--------|----------|
|
|
||||||
| PostgreSQL | База данных |
|
|
||||||
| Thoth | STT/TTS (голос↔текст) |
|
|
||||||
| Authentik | SSO для веб-интерфейса |
|
|
||||||
| Nginx + certbot | Домены и SSL |
|
|
||||||
| Gitea | Git хостинг |
|
|
||||||
| OpenClaw (Марков) | Главный агент с инструментами |
|
|
||||||
| Redis | Очереди и pub/sub |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## База данных (ключевые таблицы)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Агенты (независимые приложения)
|
|
||||||
CREATE TABLE agents (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
slug TEXT UNIQUE NOT NULL,
|
|
||||||
token TEXT UNIQUE NOT NULL, -- токен для WebSocket auth
|
|
||||||
capabilities TEXT[] NOT NULL DEFAULT '{}', -- ["coding", "analytics", ...]
|
|
||||||
subscription_mode TEXT DEFAULT 'assigned', -- all, mentions, assigned
|
|
||||||
max_concurrent INT DEFAULT 1,
|
|
||||||
timeout_seconds INT DEFAULT 600,
|
|
||||||
status TEXT DEFAULT 'offline', -- online, offline, busy
|
|
||||||
last_heartbeat TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Задачи
|
|
||||||
CREATE TABLE tasks (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
project_id UUID REFERENCES projects(id),
|
|
||||||
parent_id UUID REFERENCES tasks(id), -- подзадачи
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
status TEXT DEFAULT 'draft',
|
|
||||||
-- лейблы через связующую таблицу task_labels
|
|
||||||
requires_pr BOOLEAN DEFAULT FALSE,
|
|
||||||
assigned_agent_id UUID REFERENCES agents(id),
|
|
||||||
priority TEXT DEFAULT 'medium',
|
|
||||||
pr_url TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Зависимости задач
|
|
||||||
CREATE TABLE task_dependencies (
|
|
||||||
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
|
|
||||||
depends_on_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (task_id, depends_on_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Лейблы
|
|
||||||
CREATE TABLE labels (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT UNIQUE NOT NULL,
|
|
||||||
color TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Связка задач и лейблов
|
|
||||||
CREATE TABLE task_labels (
|
|
||||||
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
|
|
||||||
label_id UUID REFERENCES labels(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (task_id, label_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Файлы задач
|
|
||||||
CREATE TABLE task_files (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
mime_type TEXT,
|
|
||||||
file_path TEXT NOT NULL,
|
|
||||||
uploaded_by UUID, -- agent_id или null (человек)
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Управление агентами (Agent Manager)
|
|
||||||
|
|
||||||
### Принцип
|
|
||||||
|
|
||||||
**Агент = независимое приложение.** Написано на чём угодно. Подключается к Tracker по WebSocket. Tracker не управляет запуском — агент сам стартует и подключается.
|
|
||||||
|
|
||||||
### AgentManager (в Tracker)
|
|
||||||
|
|
||||||
Tracker отслеживает подключённых агентов:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Реестр агентов:
|
|
||||||
- id: uuid
|
|
||||||
name: "Кодер"
|
|
||||||
status: online | offline | busy
|
|
||||||
capabilities: ["coding", "backend"]
|
|
||||||
last_heartbeat: timestamp
|
|
||||||
current_tasks: [task_id]
|
|
||||||
```
|
|
||||||
|
|
||||||
Функции AgentManager:
|
|
||||||
1. **Регистрация** — агент подключился → auth → попал в реестр
|
|
||||||
2. **Мониторинг** — следит за heartbeat, помечает offline если пропал
|
|
||||||
3. **Назначение задач** — матчит задачу → подходящего агента по capabilities + загрузке
|
|
||||||
4. **Роутинг событий** — фильтрация по capabilities + подписке + чатам
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Открытые вопросы
|
|
||||||
|
|
||||||
1. Workspace для агентов — общий или изолированный?
|
|
||||||
2. Прогресс работы агента в реалтайме?
|
|
||||||
3. Биллинг/учёт использования API?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Приоритеты реализации
|
|
||||||
|
|
||||||
### Сделано ✅
|
|
||||||
1. [x] Tracker: REST API (проекты, задачи, агенты, лейблы)
|
|
||||||
2. [x] Tracker: базовый WebSocket (connection manager, heartbeat)
|
|
||||||
3. [x] Tracker: Docker Compose (postgres + redis + tracker)
|
|
||||||
4. [x] Web Client: канбан-доска с drag-and-drop
|
|
||||||
5. [x] Web Client: BFF с JWT авторизацией
|
|
||||||
6. [x] CI/CD: Gitea Actions auto-deploy
|
|
||||||
|
|
||||||
### В работе 🔧
|
|
||||||
7. [ ] Web Client: детальный вид задачи (описание, комменты, assignee)
|
|
||||||
8. [ ] Tracker: WebSocket протокол v2 (init handshake, роутинг событий)
|
|
||||||
|
|
||||||
### Следующее 📋
|
|
||||||
9. [ ] Tracker: чаты (лобби + проект + задача)
|
|
||||||
10. [ ] Tracker: файлы в задачах
|
|
||||||
11. [ ] Web Client: чат
|
|
||||||
12. [ ] Web Client: управление агентами
|
|
||||||
13. [ ] Первый агент (OpenClaw → Tracker)
|
|
||||||
14. [ ] Web Client: Authentik OAuth
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Документ для обсуждения и аналитики. Обновляется по мере развития идей.*
|
|
||||||
|
|
||||||
## RBAC — Система прав (TODO)
|
|
||||||
|
|
||||||
### Идея
|
|
||||||
Гранулярные права для участников проекта и агентов.
|
|
||||||
|
|
||||||
### Участники проекта
|
|
||||||
- **Owner** — создатель проекта, полный доступ (всегда по умолчанию)
|
|
||||||
- **Member** — стандартный участник, полный доступ (текущее поведение)
|
|
||||||
- **Viewer** — только чтение (уже решено ранее)
|
|
||||||
- **Custom roles** — настраиваемые роли с набором permissions
|
|
||||||
|
|
||||||
### Permissions (идеи)
|
|
||||||
- `tasks.create` — создание задач
|
|
||||||
- `tasks.edit` — редактирование задач
|
|
||||||
- `tasks.delete` — удаление задач
|
|
||||||
- `tasks.change_status` — смена статуса
|
|
||||||
- `tasks.assign` — назначение исполнителя
|
|
||||||
- `files.upload` — загрузка файлов
|
|
||||||
- `files.delete` — удаление файлов
|
|
||||||
- `chat.send` — отправка сообщений
|
|
||||||
- `project.settings` — доступ к настройкам проекта
|
|
||||||
- `members.manage` — добавление/удаление участников
|
|
||||||
|
|
||||||
### Права для агентов
|
|
||||||
- Ограничение действий через ту же систему permissions
|
|
||||||
- Например: агент может менять статус, но не может удалять задачи
|
|
||||||
- Настраивается в AgentConfig или через роли
|
|
||||||
|
|
||||||
### Реализация (потом)
|
|
||||||
- Таблица `roles` (project-scoped)
|
|
||||||
- Таблица `role_permissions` (role_id → permission)
|
|
||||||
- `project_members.role_id` вместо текущего `role: str`
|
|
||||||
- Middleware/dependency проверяет permissions на каждом endpoint
|
|
||||||
@ -1,269 +0,0 @@
|
|||||||
# Team Board — План реализации
|
|
||||||
Дата: 2026-02-22 | Обновлено: 2026-02-23
|
|
||||||
Метод: BMAD Planning
|
|
||||||
|
|
||||||
## Текущее состояние
|
|
||||||
|
|
||||||
### Tracker v0.2.0 (Python/FastAPI) ✅
|
|
||||||
- Модели: Member, AgentConfig, Project, Task, Step, Chat, Message, Attachment
|
|
||||||
- API: auth, members, projects, tasks, steps, messages, ws_router
|
|
||||||
- WS: auth, heartbeat, chat.subscribe, chat.send, message broadcast
|
|
||||||
- БД: PostgreSQL, без миграций (recreate from scratch)
|
|
||||||
|
|
||||||
### Web Client v0.2.0 (Next.js) ✅
|
|
||||||
- Страницы: login, projects list, project board
|
|
||||||
- Компоненты: KanbanBoard (drag-drop + mobile tabs), TaskModal (steps, comments, assignee), ChatPanel (REST), CreateTaskModal, CreateProjectModal, AuthGuard
|
|
||||||
- BFF: Python/FastAPI (порт 8200), JWT auth, proxy к Tracker
|
|
||||||
- WS proxy через BFF с token-based auth
|
|
||||||
- Два окружения: dev.team.uix.su (активное) / team.uix.su (placeholder)
|
|
||||||
|
|
||||||
### Что было исправлено в v0.2.0 (бывшие несоответствия)
|
|
||||||
- ✅ Member + AgentConfig вместо отдельной Agent модели
|
|
||||||
- ✅ Unified Message (chat_id + task_id + parent_id)
|
|
||||||
- ✅ Steps модель
|
|
||||||
- ✅ Watchers на Task (string[])
|
|
||||||
- ✅ Attachment модель
|
|
||||||
- ✅ repo_urls = array
|
|
||||||
- ✅ Правильные статусы (backlog/todo/in_progress/in_review/done)
|
|
||||||
- ✅ Task key на бэкенде (TE-1)
|
|
||||||
- ⬜ WS handler не фильтрует по listen_mode (Phase 3)
|
|
||||||
- ⬜ Нет project.subscribe (Phase 3)
|
|
||||||
- ⬜ Heartbeat — базовый (Phase 3)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Эпики
|
|
||||||
|
|
||||||
### Epic 1: Новые модели БД ✅ DONE
|
|
||||||
**Приоритет: MUST** (всё остальное зависит от этого)
|
|
||||||
|
|
||||||
Очистить БД, создать модели с нуля по ARCHITECTURE.md v0.4.
|
|
||||||
**Статус: Полностью выполнен в Tracker v0.2.0**
|
|
||||||
|
|
||||||
**Stories:**
|
|
||||||
|
|
||||||
**E1-S1: Member + AgentConfig**
|
|
||||||
Заменить User + Agent на единую модель Member.
|
|
||||||
```
|
|
||||||
Member: id, name, slug, type(human|agent), role(owner|member|observer|bridge),
|
|
||||||
auth_method, password_hash?, token?, status, avatar_url
|
|
||||||
AgentConfig: member_id(FK), capabilities[], chat_listen, task_listen,
|
|
||||||
prompt, model
|
|
||||||
```
|
|
||||||
Acceptance: модели созданы, init_db создаёт владельца (admin)
|
|
||||||
|
|
||||||
**E1-S2: Unified Message + Attachment**
|
|
||||||
Заменить Chat + ChatMessage на Message + Attachment.
|
|
||||||
```
|
|
||||||
Message: id, content, author_type, author_slug,
|
|
||||||
chat_id?(lobby/project), task_id?, parent_id?(thread),
|
|
||||||
mentions[], voice_url?, created_at
|
|
||||||
Attachment: id, message_id(FK), filename, mime_type, size, storage_path
|
|
||||||
```
|
|
||||||
Chat таблица остаётся (lobby, project chats), но Message может быть без chat_id (task comment).
|
|
||||||
Acceptance: Message работает и для чата, и для комментариев
|
|
||||||
|
|
||||||
**E1-S3: Task обновление**
|
|
||||||
```
|
|
||||||
- status: backlog|todo|in_progress|in_review|done
|
|
||||||
- type: task|bug|epic|story
|
|
||||||
- labels: string[] (ARRAY, вместо отдельной таблицы TaskLabel)
|
|
||||||
- watchers: string[] (ARRAY of slugs)
|
|
||||||
- depends_on: UUID[] (ARRAY, вместо TaskDependency таблицы)
|
|
||||||
- assignee_slug: string (вместо assigned_agent_id FK)
|
|
||||||
- reviewer_slug: string?
|
|
||||||
- Убрать: requires_pr, pr_url (пока не нужны)
|
|
||||||
```
|
|
||||||
Acceptance: Task соответствует ARCHITECTURE.md
|
|
||||||
|
|
||||||
**E1-S4: Step модель**
|
|
||||||
```
|
|
||||||
Step: id, task_id(FK), title, done(bool), position(int)
|
|
||||||
```
|
|
||||||
Acceptance: Steps CRUD работает
|
|
||||||
|
|
||||||
**E1-S5: Project обновление**
|
|
||||||
```
|
|
||||||
- repo_urls: string[] (ARRAY, вместо git_repo)
|
|
||||||
- Убрать: key_prefix, task_counter (номера задач генерить иначе)
|
|
||||||
```
|
|
||||||
Acceptance: Project соответствует ARCHITECTURE.md
|
|
||||||
|
|
||||||
**E1-S6: Очистка и init**
|
|
||||||
- Удалить Alembic миграции
|
|
||||||
- DROP ALL + CREATE ALL при первом запуске
|
|
||||||
- Seed: создать admin (owner), lobby chat
|
|
||||||
Acceptance: чистый старт, `docker compose down -v && up` работает
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Epic 2: REST API (MCP-ready) ✅ DONE
|
|
||||||
**Приоритет: MUST**
|
|
||||||
|
|
||||||
Обновить API эндпоинты для всех MCP tools.
|
|
||||||
**Статус: Выполнен в Tracker v0.2.0. Все CRUD эндпоинты работают. Некоторые продвинутые (take/reject/assign, files upload) — ещё не реализованы.**
|
|
||||||
|
|
||||||
**Stories:**
|
|
||||||
|
|
||||||
**E2-S1: Auth API**
|
|
||||||
- `POST /api/v1/auth/login` — логин → JWT
|
|
||||||
- `POST /api/v1/auth/register` — регистрация (disabled by default)
|
|
||||||
- Token validation middleware
|
|
||||||
Acceptance: JWT auth работает
|
|
||||||
|
|
||||||
**E2-S2: Tasks API**
|
|
||||||
- `GET /api/v1/tasks` — list (фильтры: project_id, status, assignee, labels)
|
|
||||||
- `GET /api/v1/tasks/{id}` — get
|
|
||||||
- `POST /api/v1/tasks` — create
|
|
||||||
- `PATCH /api/v1/tasks/{id}` — update
|
|
||||||
- `DELETE /api/v1/tasks/{id}` — delete
|
|
||||||
- `POST /api/v1/tasks/{id}/take` — take (атомарно)
|
|
||||||
- `POST /api/v1/tasks/{id}/reject` — reject с причиной
|
|
||||||
- `POST /api/v1/tasks/{id}/assign` — assign
|
|
||||||
- `POST /api/v1/tasks/{id}/watch` — watch
|
|
||||||
- `DELETE /api/v1/tasks/{id}/watch` — unwatch
|
|
||||||
Acceptance: все эндпоинты работают, take атомарный
|
|
||||||
|
|
||||||
**E2-S3: Steps API**
|
|
||||||
- `GET /api/v1/tasks/{id}/steps`
|
|
||||||
- `POST /api/v1/tasks/{id}/steps`
|
|
||||||
- `PATCH /api/v1/tasks/{task_id}/steps/{step_id}`
|
|
||||||
- `DELETE /api/v1/tasks/{task_id}/steps/{step_id}`
|
|
||||||
Acceptance: CRUD steps
|
|
||||||
|
|
||||||
**E2-S4: Messages API (unified)**
|
|
||||||
- `GET /api/v1/messages` — list (фильтры: chat_id, task_id, parent_id, limit/offset)
|
|
||||||
- `POST /api/v1/messages` — send (chat_id или task_id)
|
|
||||||
- `POST /api/v1/messages/{id}/reply` — thread reply
|
|
||||||
Acceptance: работает для чатов и комментариев задач
|
|
||||||
|
|
||||||
**E2-S5: Files API**
|
|
||||||
- `POST /api/v1/messages/{id}/attachments` — upload
|
|
||||||
- `GET /api/v1/attachments/{id}` — download
|
|
||||||
- `GET /api/v1/attachments` — list (фильтры: task_id, chat_id)
|
|
||||||
Acceptance: upload/download работает
|
|
||||||
|
|
||||||
**E2-S6: Projects API**
|
|
||||||
- Обновить существующий API под новые поля (repo_urls)
|
|
||||||
Acceptance: CRUD проектов работает
|
|
||||||
|
|
||||||
**E2-S7: Members API**
|
|
||||||
- `GET /api/v1/members` — list (фильтр: project_id)
|
|
||||||
- `GET /api/v1/members/{slug}` — get
|
|
||||||
- `POST /api/v1/members` — create (agent с токеном)
|
|
||||||
- `PATCH /api/v1/members/{slug}` — update
|
|
||||||
- `PATCH /api/v1/members/me/status` — update own status
|
|
||||||
Acceptance: CRUD members, генерация токенов для агентов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Epic 3: WebSocket Protocol v2 ⬜ PARTIAL
|
|
||||||
**Приоритет: MUST**
|
|
||||||
**Статус: Базовый WS (auth, chat.subscribe, chat.send, broadcast) работает. Listen_mode фильтрация, project.subscribe, heartbeat timeout — ещё нет.**
|
|
||||||
|
|
||||||
**Stories:**
|
|
||||||
|
|
||||||
**E3-S1: Auth + Heartbeat**
|
|
||||||
- `auth` → token validation → `auth.ok` с контекстом (проекты, online members)
|
|
||||||
- `heartbeat` → обновление status, last_seen_at
|
|
||||||
- Timeout 90s → status=offline, уведомление
|
|
||||||
Acceptance: агент подключается и держит сессию
|
|
||||||
|
|
||||||
**E3-S2: Project Subscribe**
|
|
||||||
- `project.subscribe` / `project.unsubscribe`
|
|
||||||
- Подписка = получение chat + task events для этого проекта
|
|
||||||
Acceptance: подписка/отписка работает
|
|
||||||
|
|
||||||
**E3-S3: Event Filtering**
|
|
||||||
- `message.new` — фильтрация по chat_listen + ownership
|
|
||||||
- `task.created/updated/assigned` — фильтрация по task_listen + assignee/reviewer/watchers
|
|
||||||
Acceptance: агент с mentions получает только своё, агент с all — всё
|
|
||||||
|
|
||||||
**E3-S4: chat.send через WS**
|
|
||||||
- Отправка сообщения через WS (помимо REST)
|
|
||||||
- Broadcast по подписчикам с фильтрацией
|
|
||||||
Acceptance: чат работает в реальном времени
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Epic 4: Web Client обновление ⬜ PARTIAL
|
|
||||||
**Приоритет: SHOULD**
|
|
||||||
|
|
||||||
**Stories:**
|
|
||||||
|
|
||||||
**E4-S1: TaskModal — комментарии**
|
|
||||||
- Лента сообщений (Message с task_id) внизу модалки
|
|
||||||
- Input для нового комментария
|
|
||||||
- Значки авторов: 👤/🤖/⚙️
|
|
||||||
Acceptance: комментарии к задаче работают
|
|
||||||
|
|
||||||
**E4-S2: TaskModal — Steps**
|
|
||||||
- Чеклист между описанием и комментариями
|
|
||||||
- Добавление/завершение/удаление steps
|
|
||||||
- Live-обновление через WS
|
|
||||||
Acceptance: steps отображаются и обновляются
|
|
||||||
|
|
||||||
**E4-S3: TaskModal — watchers**
|
|
||||||
- Кнопка "👁 Наблюдать" / "Отписаться"
|
|
||||||
- Список watchers в сайдбаре модалки
|
|
||||||
Acceptance: подписка/отписка работает
|
|
||||||
|
|
||||||
**E4-S4: Agent Management**
|
|
||||||
- Страница /agents — список агентов со статусами
|
|
||||||
- Создание агента (name, slug, capabilities) → генерация токена
|
|
||||||
- Отображение токена (один раз при создании)
|
|
||||||
Acceptance: можно создать агента и получить токен
|
|
||||||
|
|
||||||
**E4-S5: Обновление моделей в клиенте**
|
|
||||||
- api.ts: обновить типы (Member вместо Agent, Message вместо ChatMessage)
|
|
||||||
- ws.ts: обновить события (message.new, project.subscribe, heartbeat)
|
|
||||||
- ChatPanel: использовать Unified Message
|
|
||||||
Acceptance: клиент работает с новым API
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Epic 5: Picogent интеграция
|
|
||||||
**Приоритет: COULD** (после Epic 1-3)
|
|
||||||
|
|
||||||
**Stories:**
|
|
||||||
|
|
||||||
**E5-S1: MCP Tools Provider**
|
|
||||||
- Обёртка над Tracker REST API
|
|
||||||
- Все 21+ tools из BRAINSTORM-MCP-TOOLS
|
|
||||||
Acceptance: агент может вызывать tools
|
|
||||||
|
|
||||||
**E5-S2: Router v2**
|
|
||||||
- WS event → текст → единственная сессия
|
|
||||||
- Фильтрация по listen_mode на стороне picogent (дополнительно к серверной)
|
|
||||||
Acceptance: агент получает события как текст
|
|
||||||
|
|
||||||
**E5-S3: Single Session**
|
|
||||||
- Одна сессия на агента (не per-task)
|
|
||||||
- session.resume при reconnect
|
|
||||||
Acceptance: контекст сохраняется между задачами
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Порядок выполнения
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 1: Backend Foundation
|
|
||||||
E1 (модели) → E2 (API) → E3 (WS v2)
|
|
||||||
|
|
||||||
Phase 2: Frontend
|
|
||||||
E4-S5 (типы) → E4-S1 (комменты) → E4-S2 (steps) → E4-S4 (agents)
|
|
||||||
|
|
||||||
Phase 3: Agent
|
|
||||||
E5-S1 (MCP) → E5-S2 (router) → E5-S3 (session)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Оценка объёма
|
|
||||||
|
|
||||||
| Epic | Stories | Сложность | ~Часы |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| E1: Модели | 6 | Средняя | 4-6 |
|
|
||||||
| E2: REST API | 7 | Средняя | 8-12 |
|
|
||||||
| E3: WS v2 | 4 | Высокая | 6-8 |
|
|
||||||
| E4: Web Client | 5 | Средняя | 8-10 |
|
|
||||||
| E5: Picogent | 3 | Высокая | 10-14 |
|
|
||||||
| **Итого** | **25** | | **36-50** |
|
|
||||||
@ -1,666 +0,0 @@
|
|||||||
# Team Board — Research: Agent Integration
|
|
||||||
|
|
||||||
Исследование возможностей подключения AI-агентов к Team Board.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Текущие возможности OpenClaw
|
|
||||||
|
|
||||||
### 1. sessions_spawn — спавн изолированных субагентов
|
|
||||||
|
|
||||||
```
|
|
||||||
Task → Spawn → Isolated Session → Result → Announce back
|
|
||||||
```
|
|
||||||
|
|
||||||
**Параметры:**
|
|
||||||
- `task` (required) — задача для агента
|
|
||||||
- `label` — метка для логов
|
|
||||||
- `agentId` — ID агента (если разрешён в allowlist)
|
|
||||||
- `model` — переопределение модели
|
|
||||||
- `runTimeoutSeconds` — таймаут
|
|
||||||
- `cleanup` — `delete|keep`
|
|
||||||
|
|
||||||
**Поведение:**
|
|
||||||
- Создаёт новую сессию `agent:<agentId>:subagent:<uuid>`
|
|
||||||
- Субагенты имеют полный набор инструментов минус session tools
|
|
||||||
- Не блокирует: возвращает `{ status: "accepted", runId, childSessionKey }`
|
|
||||||
- После завершения — announce в исходный чат
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. sessions_send — отправка между сессиями
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent A → sessions_send → Agent B → Response
|
|
||||||
```
|
|
||||||
|
|
||||||
**Параметры:**
|
|
||||||
- `sessionKey` (required)
|
|
||||||
- `message` (required)
|
|
||||||
- `timeoutSeconds` — 0 = fire-and-forget
|
|
||||||
|
|
||||||
**Поведение:**
|
|
||||||
- Поддерживает ping-pong между агентами (до 5 итераций)
|
|
||||||
- Контекст сохраняется
|
|
||||||
- Провenance маркируется как `inter_session`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. sessions_list / sessions_history
|
|
||||||
|
|
||||||
- Список активных сессий
|
|
||||||
- История сообщений сессии
|
|
||||||
- Фильтры по типу, времени, каналу
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Multi-Agent Routing
|
|
||||||
|
|
||||||
Несколько агентов в одном gateway:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
agents:
|
|
||||||
list:
|
|
||||||
- id: main
|
|
||||||
workspace: ~/.openclaw/workspace
|
|
||||||
- id: coder
|
|
||||||
workspace: ~/projects/coder-workspace
|
|
||||||
- id: reviewer
|
|
||||||
workspace: ~/projects/reviewer-workspace
|
|
||||||
```
|
|
||||||
|
|
||||||
**Каждый агент имеет:**
|
|
||||||
- Свой workspace (файлы, AGENTS.md, SOUL.md)
|
|
||||||
- Свой state directory
|
|
||||||
- Своё хранилище сессий
|
|
||||||
- Свои auth profiles
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. ACP (Agent Client Protocol)
|
|
||||||
|
|
||||||
Стандартный протокол для подключения внешних агентов.
|
|
||||||
|
|
||||||
```
|
|
||||||
External Agent ↔ ACP ↔ OpenClaw Gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
**Реализация в OpenClaw:**
|
|
||||||
- `src/acp/server.ts` — ACP сервер
|
|
||||||
- `src/acp/client.ts` — ACP клиент
|
|
||||||
- Использует `@agentclientprotocol/sdk`
|
|
||||||
|
|
||||||
**Dangerous tools требуют approval:**
|
|
||||||
- exec, spawn, shell
|
|
||||||
- sessions_spawn, sessions_send
|
|
||||||
- gateway
|
|
||||||
- fs_write, fs_delete, fs_move
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Варианты интеграции для Team Board
|
|
||||||
|
|
||||||
### Вариант 1: OpenClaw Multi-Agent (рекомендуется)
|
|
||||||
|
|
||||||
```
|
|
||||||
Team Board API
|
|
||||||
↓
|
|
||||||
OpenClaw Gateway
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────┐
|
|
||||||
│ Agent: main (Lead) │
|
|
||||||
│ Agent: coder (Developer) │
|
|
||||||
│ Agent: reviewer (QA) │
|
|
||||||
│ Agent: analyst (Research) │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Плюсы:**
|
|
||||||
- Готовая инфраструктура
|
|
||||||
- Сохранение контекста через сессии
|
|
||||||
- Единый API
|
|
||||||
- Безопасность (sandbox, permissions)
|
|
||||||
- Инструменты (exec, browser, files)
|
|
||||||
|
|
||||||
**Реализация:**
|
|
||||||
1. Добавить агентов в `openclaw.json`
|
|
||||||
2. Team Board вызывает `sessions_spawn` / `sessions_send` через Gateway API
|
|
||||||
3. Результаты через webhook или polling
|
|
||||||
|
|
||||||
**Конфиг:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"id": "main",
|
|
||||||
"workspace": "~/.openclaw/workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "coder",
|
|
||||||
"workspace": "~/workspaces/coder",
|
|
||||||
"model": "anthropic/claude-sonnet-4-20250514"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"defaults": {
|
|
||||||
"subagents": {
|
|
||||||
"allowAgents": ["main", "coder", "reviewer"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Вариант 2: ACP Bridge для CLI агентов
|
|
||||||
|
|
||||||
Для подключения Claude Code CLI без траты API токенов.
|
|
||||||
|
|
||||||
```
|
|
||||||
Claude Code CLI (Max subscription)
|
|
||||||
↓
|
|
||||||
ACP Client Wrapper
|
|
||||||
↓
|
|
||||||
OpenClaw Gateway
|
|
||||||
↓
|
|
||||||
Team Board
|
|
||||||
```
|
|
||||||
|
|
||||||
**Компоненты:**
|
|
||||||
|
|
||||||
1. **PTY Session Manager**
|
|
||||||
```python
|
|
||||||
import pexpect
|
|
||||||
|
|
||||||
class CliSession:
|
|
||||||
def __init__(self, command="claude"):
|
|
||||||
self.child = pexpect.spawn(command)
|
|
||||||
|
|
||||||
def send(self, message: str) -> str:
|
|
||||||
self.child.sendline(message)
|
|
||||||
self.child.expect(r'\n>') # wait for prompt
|
|
||||||
return self.child.before.decode()
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **ACP Translator**
|
|
||||||
```python
|
|
||||||
class AcpCliBridge:
|
|
||||||
def handle_request(self, request):
|
|
||||||
response = self.cli_session.send(request.message)
|
|
||||||
return AcpResponse(content=response)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Сложности:**
|
|
||||||
- CLI интерактивный, не headless
|
|
||||||
- Нужно парсить вывод
|
|
||||||
- Контекст внутри CLI сессии
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Вариант 3: Direct API + Context Store
|
|
||||||
|
|
||||||
Прямые вызовы API с хранением контекста в БД.
|
|
||||||
|
|
||||||
```
|
|
||||||
Team Board
|
|
||||||
↓
|
|
||||||
Agent Service
|
|
||||||
↓
|
|
||||||
┌──────────────────────────────────┐
|
|
||||||
│ Claude API │ Codex │ Gemini │
|
|
||||||
└──────────────────────────────────┘
|
|
||||||
↓
|
|
||||||
Context Store (PostgreSQL)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Схема контекста:**
|
|
||||||
```sql
|
|
||||||
CREATE TABLE agent_contexts (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
agent_id UUID REFERENCES agents(id),
|
|
||||||
session_id UUID REFERENCES sessions(id),
|
|
||||||
messages JSONB, -- история сообщений
|
|
||||||
metadata JSONB, -- system prompt, tools, etc.
|
|
||||||
created_at TIMESTAMPTZ,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**При каждом запросе:**
|
|
||||||
1. Загрузить контекст из БД
|
|
||||||
2. Добавить новое сообщение
|
|
||||||
3. Вызвать API с полным контекстом
|
|
||||||
4. Сохранить ответ в БД
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Web Terminal Interface
|
|
||||||
|
|
||||||
### Архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
Browser (xterm.js)
|
|
||||||
↓ WebSocket
|
|
||||||
Team Board Frontend
|
|
||||||
↓ WebSocket
|
|
||||||
Team Board Backend (Session Manager)
|
|
||||||
↓ PTY / API
|
|
||||||
Agent (OpenClaw / CLI / API)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Компоненты
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- xterm.js — терминальный эмулятор
|
|
||||||
- WebSocket клиент
|
|
||||||
- Session selector UI
|
|
||||||
|
|
||||||
**Backend:**
|
|
||||||
- WebSocket сервер
|
|
||||||
- Session Manager
|
|
||||||
- PTY spawner (для CLI агентов)
|
|
||||||
- OpenClaw Gateway client (для OpenClaw агентов)
|
|
||||||
|
|
||||||
### Пример кода (Backend)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from fastapi import WebSocket
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class AgentSessionManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.sessions = {}
|
|
||||||
|
|
||||||
async def create_session(self, agent_type: str, config: dict):
|
|
||||||
if agent_type == "openclaw":
|
|
||||||
return OpenClawSession(config)
|
|
||||||
elif agent_type == "cli":
|
|
||||||
return CliSession(config)
|
|
||||||
elif agent_type == "api":
|
|
||||||
return ApiSession(config)
|
|
||||||
|
|
||||||
async def handle_websocket(self, ws: WebSocket, session_id: str):
|
|
||||||
session = self.sessions[session_id]
|
|
||||||
|
|
||||||
async for message in ws.iter_text():
|
|
||||||
response = await session.send(message)
|
|
||||||
await ws.send_text(response)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Сравнение вариантов
|
|
||||||
|
|
||||||
| Критерий | OpenClaw Multi-Agent | ACP CLI Bridge | Direct API |
|
|
||||||
|----------|---------------------|----------------|------------|
|
|
||||||
| Сложность | Низкая | Высокая | Средняя |
|
|
||||||
| Контекст | ✓ Автоматический | ✓ В CLI | Ручной |
|
|
||||||
| Инструменты | ✓ Полный набор | ✓ CLI tools | ✗ Нет |
|
|
||||||
| API токены | Нужны | Не нужны (Max) | Нужны |
|
|
||||||
| Готовность | Сейчас | Требует разработки | Требует разработки |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Рекомендуемый план
|
|
||||||
|
|
||||||
### Фаза 1: OpenClaw Multi-Agent (быстрый старт)
|
|
||||||
|
|
||||||
1. Добавить агентов в конфиг OpenClaw
|
|
||||||
2. Team Board API вызывает Gateway
|
|
||||||
3. Использовать `sessions_spawn` для задач
|
|
||||||
4. Использовать `sessions_send` для коммуникации
|
|
||||||
|
|
||||||
**Результат:** Рабочая система с несколькими агентами
|
|
||||||
|
|
||||||
### Фаза 2: Web Terminal UI
|
|
||||||
|
|
||||||
1. xterm.js на фронте
|
|
||||||
2. WebSocket сервер
|
|
||||||
3. Прямое взаимодействие с агентами
|
|
||||||
|
|
||||||
**Результат:** Terminal-like опыт в браузере
|
|
||||||
|
|
||||||
### Фаза 3: ACP CLI Bridge (опционально)
|
|
||||||
|
|
||||||
1. PTY wrapper для Claude Code CLI
|
|
||||||
2. ACP транслятор
|
|
||||||
3. Интеграция с Gateway
|
|
||||||
|
|
||||||
**Результат:** CLI агенты без API токенов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Открытые вопросы
|
|
||||||
|
|
||||||
1. **Headless mode в Claude Code CLI?**
|
|
||||||
- Нужно проверить `claude --help`
|
|
||||||
- Возможно есть `--message` или `--non-interactive`
|
|
||||||
|
|
||||||
2. **Как шарить файлы между агентами?**
|
|
||||||
- Общий workspace?
|
|
||||||
- Git как source of truth?
|
|
||||||
- Копирование через API?
|
|
||||||
|
|
||||||
3. **Rate limiting для API агентов?**
|
|
||||||
- Очередь задач
|
|
||||||
- Приоритеты
|
|
||||||
- Backoff при ошибках
|
|
||||||
|
|
||||||
4. **Мониторинг агентов?**
|
|
||||||
- Статус (idle/working/error)
|
|
||||||
- Использование токенов
|
|
||||||
- Время выполнения
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent Management System
|
|
||||||
|
|
||||||
### Agent Registry (БД)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE agents (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL, -- "Coder", "Analyst"
|
|
||||||
slug TEXT UNIQUE NOT NULL, -- "coder", "analyst"
|
|
||||||
|
|
||||||
-- Настройки
|
|
||||||
model TEXT, -- "claude-opus-4-5", "claude-sonnet"
|
|
||||||
system_prompt TEXT, -- роль/персона
|
|
||||||
max_concurrent INT DEFAULT 1, -- макс параллельных задач
|
|
||||||
|
|
||||||
-- Статус
|
|
||||||
status TEXT DEFAULT 'idle', -- idle, working, error, disabled
|
|
||||||
current_tasks INT DEFAULT 0,
|
|
||||||
|
|
||||||
-- Мета
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web UI: Agent Config
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ Agents [+ Add] │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ 🟢 Coder claude-opus idle [⚙] │
|
|
||||||
│ 🟢 Reviewer claude-sonnet idle [⚙] │
|
|
||||||
│ 🟡 Analyst claude-opus working[⚙] │
|
|
||||||
│ ⚫ Tester claude-sonnet disabled[⚙] │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task Assignment Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
1. User создаёт задачу в Web UI
|
|
||||||
2. Назначает агента (или Auto → Lead решает)
|
|
||||||
3. Team Board → POST /hooks/agent → OpenClaw
|
|
||||||
4. OpenClaw проверяет лимиты, спавнит субагента
|
|
||||||
5. Субагент работает, результат → callback
|
|
||||||
6. Team Board обновляет задачу в БД
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenClaw Webhooks API
|
|
||||||
|
|
||||||
### Конфигурация
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"enabled": true,
|
|
||||||
"token": "${OPENCLAW_HOOKS_TOKEN}",
|
|
||||||
"path": "/hooks",
|
|
||||||
"defaultSessionKey": "hook:team-board",
|
|
||||||
"allowRequestSessionKey": true,
|
|
||||||
"allowedSessionKeyPrefixes": ["hook:", "task:"],
|
|
||||||
"allowedAgentIds": ["main", "coder", "reviewer"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Эндпоинты
|
|
||||||
|
|
||||||
#### POST /hooks/wake — Разбудить основную сессию
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:18789/hooks/wake \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"text": "Новая задача в Team Board", "mode": "now"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Параметры:**
|
|
||||||
- `text` (required) — описание события
|
|
||||||
- `mode` — `now` (сразу) или `next-heartbeat` (при след. проверке)
|
|
||||||
|
|
||||||
#### POST /hooks/agent — Запустить изолированную сессию
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:18789/hooks/agent \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"message": "Напиши функцию сортировки на Python",
|
|
||||||
"name": "TeamBoard",
|
|
||||||
"agentId": "main",
|
|
||||||
"sessionKey": "task:abc123",
|
|
||||||
"deliver": true,
|
|
||||||
"channel": "telegram",
|
|
||||||
"timeoutSeconds": 300
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Параметры:**
|
|
||||||
- `message` (required) — промпт для агента
|
|
||||||
- `name` — название хука для логов
|
|
||||||
- `agentId` — ID агента (main, coder, etc.)
|
|
||||||
- `sessionKey` — ключ сессии для отслеживания
|
|
||||||
- `deliver` — отправить ответ в канал
|
|
||||||
- `channel` — канал для ответа (telegram, discord, etc.)
|
|
||||||
- `model` — переопределение модели
|
|
||||||
- `thinking` — уровень размышлений (low, medium, high)
|
|
||||||
- `timeoutSeconds` — таймаут выполнения
|
|
||||||
|
|
||||||
### Аутентификация
|
|
||||||
|
|
||||||
```
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
# или
|
|
||||||
x-openclaw-token: <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ Query-string токены (`?token=...`) запрещены.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Team Board → OpenClaw Integration
|
|
||||||
|
|
||||||
### Архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ Team Board │
|
|
||||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
||||||
│ │ Web UI │ │ Tasks │ │ Agents │ │
|
|
||||||
│ │ (Next.js) │ │ Service │ │ Service │ │
|
|
||||||
│ └─────────────┘ └──────┬──────┘ └──────┬──────┘ │
|
|
||||||
│ │ │ │
|
|
||||||
│ └────────┬────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ┌────────▼────────┐ │
|
|
||||||
│ │ OpenClaw │ │
|
|
||||||
│ │ Client │ │
|
|
||||||
│ └────────┬────────┘ │
|
|
||||||
└───────────────────────────────────┼─────────────────────────┘
|
|
||||||
│ HTTP
|
|
||||||
▼
|
|
||||||
┌───────────────────────────────────────────────────────────────┐
|
|
||||||
│ OpenClaw Gateway │
|
|
||||||
│ localhost:18789 │
|
|
||||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
||||||
│ │ /hooks/wake │ │/hooks/agent │ │ Sessions │ │
|
|
||||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Agent: main (Марков) │ │
|
|
||||||
│ │ - sessions_spawn → субагенты │ │
|
|
||||||
│ │ - sessions_send → коммуникация │ │
|
|
||||||
│ │ - sessions_history → история │ │
|
|
||||||
│ └─────────────────────────────────────────────────────────┘ │
|
|
||||||
└───────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenClaw Client (Python)
|
|
||||||
|
|
||||||
```python
|
|
||||||
import httpx
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
class OpenClawClient:
|
|
||||||
def __init__(self, base_url: str, token: str):
|
|
||||||
self.base_url = base_url
|
|
||||||
self.headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
async def wake(self, text: str, mode: str = "now"):
|
|
||||||
"""Разбудить основную сессию"""
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{self.base_url}/hooks/wake",
|
|
||||||
headers=self.headers,
|
|
||||||
json={"text": text, "mode": mode}
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def run_agent(
|
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
session_key: str,
|
|
||||||
agent_id: str = "main",
|
|
||||||
deliver: bool = False,
|
|
||||||
timeout: int = 300
|
|
||||||
):
|
|
||||||
"""Запустить задачу в изолированной сессии"""
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{self.base_url}/hooks/agent",
|
|
||||||
headers=self.headers,
|
|
||||||
json={
|
|
||||||
"message": message,
|
|
||||||
"name": "TeamBoard",
|
|
||||||
"agentId": agent_id,
|
|
||||||
"sessionKey": session_key,
|
|
||||||
"deliver": deliver,
|
|
||||||
"timeoutSeconds": timeout
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task Execution Flow
|
|
||||||
|
|
||||||
```python
|
|
||||||
async def execute_task(task: Task, agent: Agent):
|
|
||||||
client = OpenClawClient(
|
|
||||||
base_url="http://localhost:18789",
|
|
||||||
token=settings.OPENCLAW_TOKEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Формируем промпт
|
|
||||||
prompt = f"""
|
|
||||||
Задача: {task.title}
|
|
||||||
|
|
||||||
Описание: {task.description}
|
|
||||||
|
|
||||||
Контекст проекта: {task.project.brief}
|
|
||||||
|
|
||||||
Выполни задачу и верни результат.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Запускаем агента
|
|
||||||
result = await client.run_agent(
|
|
||||||
message=prompt,
|
|
||||||
session_key=f"task:{task.id}",
|
|
||||||
agent_id=agent.slug,
|
|
||||||
timeout=600
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обновляем статус
|
|
||||||
task.status = "in_progress"
|
|
||||||
task.openclaw_session = result.get("sessionKey")
|
|
||||||
await task.save()
|
|
||||||
|
|
||||||
return result
|
|
||||||
```
|
|
||||||
|
|
||||||
### Получение результатов
|
|
||||||
|
|
||||||
**Вариант 1: Polling**
|
|
||||||
```python
|
|
||||||
async def poll_task_result(task: Task):
|
|
||||||
client = OpenClawClient(...)
|
|
||||||
|
|
||||||
# Получаем историю сессии
|
|
||||||
history = await client.get_session_history(task.openclaw_session)
|
|
||||||
|
|
||||||
# Парсим последний ответ
|
|
||||||
last_message = history["messages"][-1]
|
|
||||||
if last_message["role"] == "assistant":
|
|
||||||
task.result = last_message["content"]
|
|
||||||
task.status = "completed"
|
|
||||||
await task.save()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Вариант 2: Callback Webhook**
|
|
||||||
```python
|
|
||||||
# Team Board получает callback от OpenClaw
|
|
||||||
@app.post("/api/callbacks/openclaw")
|
|
||||||
async def openclaw_callback(data: dict):
|
|
||||||
session_key = data.get("sessionKey")
|
|
||||||
result = data.get("result")
|
|
||||||
|
|
||||||
# Находим задачу по session_key
|
|
||||||
task = await Task.find_by_session(session_key)
|
|
||||||
task.result = result
|
|
||||||
task.status = "completed"
|
|
||||||
await task.save()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Текущий статус OpenClaw
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Gateway endpoint
|
|
||||||
http://localhost:18789
|
|
||||||
|
|
||||||
# Текущий токен
|
|
||||||
cat ~/.openclaw/openclaw.json | jq '.gateway.auth.token'
|
|
||||||
|
|
||||||
# Webhooks пока не настроены — нужно включить
|
|
||||||
```
|
|
||||||
|
|
||||||
### Для активации webhooks:
|
|
||||||
|
|
||||||
```json
|
|
||||||
// ~/.openclaw/openclaw.json
|
|
||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"enabled": true,
|
|
||||||
"token": "team-board-secret-token",
|
|
||||||
"path": "/hooks"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Документ обновляется по мере исследования.*
|
|
||||||
@ -1,292 +0,0 @@
|
|||||||
# Team Board — Протокол взаимодействия
|
|
||||||
|
|
||||||
## Транспорт
|
|
||||||
|
|
||||||
Два способа подключения агентов:
|
|
||||||
|
|
||||||
### 1. WebSocket (основной)
|
|
||||||
```
|
|
||||||
ws://{host}:8100/ws?client_type={type}&client_id={slug}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. HTTP Callback (альтернативный)
|
|
||||||
Агент регистрируется через REST API, указывает `callback_url`. Tracker отправляет события POST-запросами на этот URL.
|
|
||||||
|
|
||||||
```
|
|
||||||
POST {callback_url}/events
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"event": "chat.message", ...}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Типы клиентов (`client_type`)
|
|
||||||
|
|
||||||
| Тип | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| `human` | Веб-клиент (браузер) |
|
|
||||||
| `agent` | AI-агент (Runner, кодер и т.д.) |
|
|
||||||
| `bridge` | Мост в другую систему (Telegram, Slack) |
|
|
||||||
|
|
||||||
## Регистрация агента
|
|
||||||
|
|
||||||
### Через REST API (HTTP-агенты и первичная регистрация)
|
|
||||||
```
|
|
||||||
POST /api/v1/agents/register
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Кодер",
|
|
||||||
"slug": "coder",
|
|
||||||
"description": "AI-агент для написания кода",
|
|
||||||
"prompt": "Ты — агент-кодер...",
|
|
||||||
"capabilities": ["code", "python", "git"],
|
|
||||||
"connection_type": "websocket|http_callback",
|
|
||||||
"callback_url": "http://agent-host:9000/events", // только для http_callback
|
|
||||||
"port": 9000 // порт агента (информационно)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "uuid",
|
|
||||||
"slug": "coder",
|
|
||||||
"token": "agent-xxx",
|
|
||||||
"status": "registered"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Через WebSocket (при подключении)
|
|
||||||
После установки WS-соединения агент отправляет `auth`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "auth",
|
|
||||||
"token": "agent-xxx",
|
|
||||||
"name": "Кодер",
|
|
||||||
"description": "AI-агент для написания кода",
|
|
||||||
"prompt": "Ты — агент-кодер...",
|
|
||||||
"capabilities": ["code", "python", "git"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tracker обновляет данные агента и отвечает:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "auth.ok",
|
|
||||||
"agent_id": "uuid",
|
|
||||||
"init": {
|
|
||||||
"chats": ["lobby-uuid"],
|
|
||||||
"agents_online": ["coder", "reviewer"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Модель данных: Agent
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent:
|
|
||||||
id UUID (PK)
|
|
||||||
name string — отображаемое имя
|
|
||||||
slug string — уникальный идентификатор (@slug)
|
|
||||||
token string — токен аутентификации
|
|
||||||
description string — описание агента
|
|
||||||
prompt text — системный промпт агента
|
|
||||||
capabilities string[] — возможности ["code", "python", "review"]
|
|
||||||
connection_type enum — websocket | http_callback | bridge
|
|
||||||
callback_url string? — URL для HTTP callback
|
|
||||||
role enum — owner | agent | bridge | observer
|
|
||||||
permissions string[] — ["create_tasks", "assign_tasks", "send_messages", "manage_agents"]
|
|
||||||
status enum — online | offline | busy
|
|
||||||
last_seen_at timestamp — последняя активность
|
|
||||||
created_at timestamp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Роли
|
|
||||||
|
|
||||||
| Роль | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `owner` | Владелец доски. Полные права. Один на инстанс. |
|
|
||||||
| `agent` | AI-агент. Выполняет задачи, пишет в чат. |
|
|
||||||
| `bridge` | Мост (Telegram, Slack). Пересылает сообщения, не берёт задачи. |
|
|
||||||
| `observer` | Только чтение. Получает события, но не может действовать. |
|
|
||||||
|
|
||||||
### Права (permissions)
|
|
||||||
|
|
||||||
| Право | Описание |
|
|
||||||
|-------|----------|
|
|
||||||
| `send_messages` | Отправка сообщений в чаты |
|
|
||||||
| `create_tasks` | Создание новых задач |
|
|
||||||
| `assign_tasks` | Назначение задач другим агентам |
|
|
||||||
| `update_tasks` | Изменение статуса/описания задач |
|
|
||||||
| `manage_agents` | Управление другими агентами |
|
|
||||||
| `manage_projects` | Создание/изменение проектов |
|
|
||||||
|
|
||||||
По умолчанию агент получает: `send_messages`, `update_tasks`.
|
|
||||||
Bridge получает: `send_messages`.
|
|
||||||
Owner получает всё.
|
|
||||||
|
|
||||||
## События: Клиент → Сервер
|
|
||||||
|
|
||||||
### auth
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "auth",
|
|
||||||
"token": "agent-xxx",
|
|
||||||
"name": "Кодер",
|
|
||||||
"capabilities": ["code"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### chat.subscribe
|
|
||||||
```json
|
|
||||||
{"event": "chat.subscribe", "chat_id": "uuid"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### chat.send
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "chat.send",
|
|
||||||
"chat_id": "uuid",
|
|
||||||
"content": "текст сообщения",
|
|
||||||
"sender_type": "agent|bridge|human",
|
|
||||||
"sender_name": "Кодер",
|
|
||||||
"mentions": ["reviewer"] // @slug упоминания
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### agent.heartbeat
|
|
||||||
```json
|
|
||||||
{"event": "agent.heartbeat", "status": "idle|busy"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.take
|
|
||||||
Агент берёт задачу в работу.
|
|
||||||
```json
|
|
||||||
{"event": "task.take", "task_id": "uuid"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.complete
|
|
||||||
Агент завершил задачу.
|
|
||||||
```json
|
|
||||||
{"event": "task.complete", "task_id": "uuid"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.comment
|
|
||||||
Комментарий к задаче.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "task.comment",
|
|
||||||
"task_id": "uuid",
|
|
||||||
"content": "Готово, коммит abc123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## События: Сервер → Клиент (broadcast)
|
|
||||||
|
|
||||||
Все события рассылаются **всем подключённым клиентам** (WS + HTTP callback).
|
|
||||||
|
|
||||||
### chat.message
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "chat.message",
|
|
||||||
"id": "msg-uuid",
|
|
||||||
"chat_id": "uuid",
|
|
||||||
"sender_type": "human|agent|system|bridge",
|
|
||||||
"sender_id": "uuid|null",
|
|
||||||
"sender_name": "Имя",
|
|
||||||
"sender_slug": "slug|null",
|
|
||||||
"content": "текст",
|
|
||||||
"mentions": ["coder"],
|
|
||||||
"created_at": "2026-02-20T12:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.created
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "task.created",
|
|
||||||
"task_id": "uuid",
|
|
||||||
"title": "Название задачи",
|
|
||||||
"project_id": "uuid",
|
|
||||||
"status": "todo",
|
|
||||||
"assigned_to": "slug|null"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.updated
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "task.updated",
|
|
||||||
"task_id": "uuid",
|
|
||||||
"changes": {"status": "in_progress", "assigned_to": "coder"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### task.assigned
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "task.assigned",
|
|
||||||
"task_id": "uuid",
|
|
||||||
"title": "Название",
|
|
||||||
"description": "Описание",
|
|
||||||
"assigned_to": "coder"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### agent.connected / agent.disconnected
|
|
||||||
```json
|
|
||||||
{"event": "agent.connected", "slug": "coder", "name": "Кодер"}
|
|
||||||
{"event": "agent.disconnected", "slug": "coder", "name": "Кодер"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### system.notification
|
|
||||||
Системные уведомления (статус изменился, дедлайн и т.д.).
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "system.notification",
|
|
||||||
"type": "task_status_changed|agent_joined|deadline_approaching",
|
|
||||||
"message": "Задача #42 перешла в статус 'in_review'",
|
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Чаты
|
|
||||||
|
|
||||||
| Тип | Описание |
|
|
||||||
|-----|----------|
|
|
||||||
| `lobby` | Общий чат (один на инстанс) |
|
|
||||||
| `project` | Чат проекта (один на проект) |
|
|
||||||
| `task` | Чат задачи (один на задачу) |
|
|
||||||
|
|
||||||
### REST API
|
|
||||||
```
|
|
||||||
GET /api/v1/chats/lobby — lobby чат
|
|
||||||
GET /api/v1/chats/{id}/messages?limit=20 — история
|
|
||||||
POST /api/v1/chats/{id}/messages — отправить
|
|
||||||
```
|
|
||||||
|
|
||||||
## Telegram Bridge
|
|
||||||
|
|
||||||
Bridge-агент с `connection_type=bridge`:
|
|
||||||
|
|
||||||
1. Подключается по WebSocket как `client_type=bridge`
|
|
||||||
2. Подписывается на нужные чаты
|
|
||||||
3. **Tracker → Telegram**: `chat.message` → форматирует и отправляет в группу
|
|
||||||
4. **Telegram → Tracker**: сообщение из группы → `chat.send` с `sender_type=bridge`
|
|
||||||
5. **Системные события**: `task.created`, `task.updated` → уведомления в группу
|
|
||||||
6. **Упоминания**: парсит `@slug` из Telegram → `mentions` в `chat.send`
|
|
||||||
|
|
||||||
### Формат в Telegram
|
|
||||||
```
|
|
||||||
👤 Eugene: Сделай рефактор auth модуля
|
|
||||||
🤖 Кодер: Принял, начинаю работу
|
|
||||||
⚙️ Задача #42 → in_progress (Кодер)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Инфраструктура
|
|
||||||
|
|
||||||
- Tracker: `localhost:8100` (Docker, не экспонирован наружу)
|
|
||||||
- PostgreSQL: `localhost:5433` (team_board_dev)
|
|
||||||
- Redis: `localhost:6380` (пока не используется)
|
|
||||||
- Web Client: `team.uix.su` (Next.js:3100 + BFF:8200)
|
|
||||||
- Lobby Chat ID: `25c20eaa-fbf1-4259-8501-0854cc926d0d`
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
# WebSocket протокол Team Board
|
|
||||||
|
|
||||||
## Общая схема
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────┐ ┌─────┐ ┌─────────┐
|
|
||||||
│ Next.js │ ←http→ │ BFF │ ←ws→ │ Tracker │
|
|
||||||
│ (браузер)│ │ │ │ │
|
|
||||||
└──────────┘ └─────┘ └─────────┘
|
|
||||||
↑ ws (JWT) ↑ ws (agent token)
|
|
||||||
Браузер Agent Instance
|
|
||||||
```
|
|
||||||
|
|
||||||
### Два типа клиентов:
|
|
||||||
|
|
||||||
| Клиент | Подключение | Аутентификация |
|
|
||||||
|--------|-------------|----------------|
|
|
||||||
| **Человек** (браузер) | `wss://team.uix.su/ws?token=<JWT>` → BFF → Tracker | JWT токен |
|
|
||||||
| **Агент** | `ws://tracker:8100/ws?token=<agent_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:<slug>` — все события проекта (задачи, чат)
|
|
||||||
- `task:<key>` — события конкретной задачи (комментарии, файлы, статус)
|
|
||||||
- `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=<JWT>
|
|
||||||
2. BFF validates JWT
|
|
||||||
3. BFF → Tracker: ws://tracker:8100/ws?client_type=human&client_id=<username>
|
|
||||||
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=<agent_session_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:<slug>` |
|
|
||||||
| `task.updated` | Подписчики `project:<slug>` + `task:<key>` |
|
|
||||||
| `comment.created` | Подписчики `task:<key>` |
|
|
||||||
| `chat.message` | Подписчики `project:<slug>` (для чата проекта) |
|
|
||||||
| `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" }
|
|
||||||
```
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
# Team Board — Статусы брейнштормов
|
|
||||||
# Обновлено: 2026-02-22
|
|
||||||
|
|
||||||
topics:
|
|
||||||
|
|
||||||
# === ГОТОВО ===
|
|
||||||
|
|
||||||
agents_architecture:
|
|
||||||
name: "Агентная архитектура"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-AGENTS-2026-02-20.md
|
|
||||||
summary: "Один binary (picogent), роль=конфиг, git workflow, MCP tools, checkpoint pattern"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
name: "Задачи (Tasks)"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-TASKS-2026-02-20.md
|
|
||||||
summary: "Поля, статусы, подзадачи vs этапы, reject, auto-assign, блокеры"
|
|
||||||
|
|
||||||
projects:
|
|
||||||
name: "Проекты (Projects)"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-PROJECTS-2026-02-20.md
|
|
||||||
summary: "Вкладки, multi-repo, онбординг, listen modes, label matching"
|
|
||||||
|
|
||||||
protocol:
|
|
||||||
name: "Протокол Picogent↔Tracker"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-PROTOCOL-2026-02-20.md
|
|
||||||
summary: "WS=real-time, REST=мутации через MCP, router=text relay, one session per agent"
|
|
||||||
|
|
||||||
chats:
|
|
||||||
name: "Чаты"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-CHATS-2026-02-21.md
|
|
||||||
summary: "Lobby/project/task, threads, голосовые, реакции TBD"
|
|
||||||
|
|
||||||
misc:
|
|
||||||
name: "Разное (notifications, rollback, memory, deploy)"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-MISC-2026-02-21.md
|
|
||||||
summary: "Telegram bridge, OpenClaw bridge, agent memory, deploy"
|
|
||||||
|
|
||||||
microservices:
|
|
||||||
name: "Микросервисы"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-MICROSERVICES-2026-02-21.md
|
|
||||||
summary: "Event Bus (Redis Streams — идея), Saga, idempotency, Circuit Breaker"
|
|
||||||
|
|
||||||
prompts:
|
|
||||||
name: "Промпты агентов"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-PROMPTS-2026-02-21.md
|
|
||||||
summary: "Системные промпты ролей, guidelines"
|
|
||||||
|
|
||||||
# === ЗАПЛАНИРОВАНО ===
|
|
||||||
|
|
||||||
file_storage:
|
|
||||||
name: "Файловое хранилище"
|
|
||||||
status: done
|
|
||||||
doc: null
|
|
||||||
summary: "Файлы = вложения к задачам, сообщениям, комментариям, документации. Отдельного сервиса нет."
|
|
||||||
|
|
||||||
members_model:
|
|
||||||
name: "Модель участников (Members)"
|
|
||||||
status: done
|
|
||||||
doc: null
|
|
||||||
summary: "Упрощённо: один пользователь + агенты с токенами. Веб-клиент генерирует токены агентов."
|
|
||||||
|
|
||||||
mcp_tools:
|
|
||||||
name: "MCP Tools для Tracker"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-MCP-TOOLS-2026-02-22.md
|
|
||||||
summary: "21 tool: tasks, steps, messages, files, projects, members. Git не в MCP."
|
|
||||||
|
|
||||||
web_ui:
|
|
||||||
name: "Web UI детали"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-UI-2026-02-22.md
|
|
||||||
summary: "Unified Message (чат+комменты), steps в TaskModal, threads только в чате"
|
|
||||||
|
|
||||||
telegram_bridge:
|
|
||||||
name: "Telegram Bridge"
|
|
||||||
status: deferred
|
|
||||||
doc: null
|
|
||||||
notes: |
|
|
||||||
- Один бот, WS к tracker
|
|
||||||
- Топики в Telegram = проекты
|
|
||||||
- Форматирование сообщений
|
|
||||||
- Telegram→Tracker и обратно
|
|
||||||
|
|
||||||
security:
|
|
||||||
name: "Безопасность"
|
|
||||||
status: deferred
|
|
||||||
doc: null
|
|
||||||
notes: |
|
|
||||||
- Allowed paths, rate limiting
|
|
||||||
- Audit log
|
|
||||||
- Token rotation
|
|
||||||
|
|
||||||
ci_cd_deploy:
|
|
||||||
name: "CI/CD и деплой"
|
|
||||||
status: deferred
|
|
||||||
doc: null
|
|
||||||
notes: |
|
|
||||||
- systemd для picogent
|
|
||||||
- Docker Compose для tracker stack
|
|
||||||
- Автоперезапуск, логирование
|
|
||||||
- Scaling
|
|
||||||
|
|
||||||
ws_protocol_v2:
|
|
||||||
name: "WebSocket протокол v2 (реализация)"
|
|
||||||
status: done
|
|
||||||
doc: BRAINSTORM-WS-V2-2026-02-22.md
|
|
||||||
summary: "Раздельные listen modes (chat/task), watchers, project.subscribe, unified message.new"
|
|
||||||
45
docker-compose.test.yml
Normal file
45
docker-compose.test.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Тестовое окружение Team Board
|
||||||
|
# Поднимает полный стек на изолированной БД и портах.
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# ./test.sh — поднять, прогнать тесты, убить
|
||||||
|
# docker compose -f docker-compose.test.yml up -d — поднять вручную
|
||||||
|
# docker compose -f docker-compose.test.yml down -v — убить
|
||||||
|
#
|
||||||
|
# Порты (тестовые):
|
||||||
|
# Tracker API: 8101
|
||||||
|
# PostgreSQL: 5433 (изолированный)
|
||||||
|
|
||||||
|
services:
|
||||||
|
db-test:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: team_board_test
|
||||||
|
POSTGRES_USER: team_board
|
||||||
|
POSTGRES_PASSWORD: team_board
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/postgresql/data # RAM-диск — быстро, дропается при down
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U team_board -d team_board_test"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 15
|
||||||
|
|
||||||
|
tracker-test:
|
||||||
|
build: ./tracker
|
||||||
|
ports:
|
||||||
|
- "8101:8100"
|
||||||
|
environment:
|
||||||
|
TRACKER_DATABASE_URL: postgresql+asyncpg://team_board:team_board@db-test:5432/team_board_test
|
||||||
|
TRACKER_ENV: dev
|
||||||
|
TRACKER_JWT_SECRET: test-secret
|
||||||
|
depends_on:
|
||||||
|
db-test:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python3 -c \"import socket; s=socket.create_connection(('localhost',8100),2); s.close()\""]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 15
|
||||||
1
docs
Submodule
1
docs
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d3b9aead966bf1fed95ced5e5705c61e817cd45a
|
||||||
@ -1,131 +0,0 @@
|
|||||||
# Миграция: messages → events
|
|
||||||
|
|
||||||
## Цель
|
|
||||||
Единая таблица `events` вместо `messages` + `task_actions`. Один event = одно действие в системе.
|
|
||||||
|
|
||||||
## Новая схема
|
|
||||||
|
|
||||||
### Таблица `events`
|
|
||||||
```sql
|
|
||||||
CREATE TABLE events (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id),
|
|
||||||
type VARCHAR(50) NOT NULL, -- см. типы ниже
|
|
||||||
actor_id UUID REFERENCES members(id), -- кто сделал
|
|
||||||
task_id UUID REFERENCES tasks(id) ON DELETE CASCADE, -- nullable
|
|
||||||
parent_id UUID REFERENCES events(id), -- для тредов
|
|
||||||
payload JSONB NOT NULL DEFAULT '{}',
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_events_project ON events(project_id, created_at);
|
|
||||||
CREATE INDEX idx_events_task ON events(task_id, created_at);
|
|
||||||
CREATE INDEX idx_events_type ON events(type);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Таблица `attachments` (без изменений, переименуем FK)
|
|
||||||
```sql
|
|
||||||
ALTER TABLE attachments RENAME COLUMN message_id TO event_id;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Типы событий
|
|
||||||
| type | payload | Где видно |
|
|
||||||
|------|---------|-----------|
|
|
||||||
| `chat_message` | `{content, mentions?, thinking?, voice_url?}` | Проектный чат |
|
|
||||||
| `task_comment` | `{content, mentions?, thinking?, tool_log?}` | Комментарии задачи |
|
|
||||||
| `task_created` | `{title, status, priority, assignee?}` | Чат + задача |
|
|
||||||
| `task_status` | `{from, to}` | Чат + задача |
|
|
||||||
| `task_assigned` | `{assignee, previous?}` | Чат + задача |
|
|
||||||
| `task_unassigned` | `{previous}` | Чат + задача |
|
|
||||||
| `task_updated` | `{field, from, to}` | Задача |
|
|
||||||
| `task_label_add` | `{label}` | Задача |
|
|
||||||
| `task_label_remove` | `{label}` | Задача |
|
|
||||||
|
|
||||||
### Что удаляем
|
|
||||||
- Таблица `messages` → заменена `events`
|
|
||||||
- Таблица `task_actions` → заменена `events` (type = task_*)
|
|
||||||
- Таблица `chats` → **удаляем** (project_id в events заменяет chat routing)
|
|
||||||
- Колонка `author_type` → заменена на `type` + `actor_id` (system events = actor_id NULL)
|
|
||||||
|
|
||||||
### Что НЕ меняем
|
|
||||||
- `tasks`, `projects`, `members`, `agent_configs`, `labels`, `steps`, `project_files`, `task_labels`, `task_links`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## План миграции (6 этапов)
|
|
||||||
|
|
||||||
### Этап 1: Backend — модели
|
|
||||||
- [ ] Создать `models/event.py` с Event model
|
|
||||||
- [ ] Удалить `models/message.py`, `models/task_action.py`
|
|
||||||
- [ ] Удалить `models/chat.py`
|
|
||||||
- [ ] Обновить `__init__.py` — импорты
|
|
||||||
|
|
||||||
### Этап 2: Backend — API
|
|
||||||
- [ ] Переписать `api/messages.py` → `api/events.py`
|
|
||||||
- `POST /api/v1/events` — создать event (chat_message, task_comment)
|
|
||||||
- `GET /api/v1/events?project_id=X&types=chat_message,task_status,...&limit=N` — лента проекта
|
|
||||||
- `GET /api/v1/events?task_id=X` — лента задачи
|
|
||||||
- [ ] Обновить `api/tasks.py` — создавать events вместо messages + task_actions
|
|
||||||
- Одна запись вместо двух (task comment + chat message → один event)
|
|
||||||
- [ ] Обновить `api/schemas.py` — EventOut, EventCreate
|
|
||||||
- [ ] Удалить эндпоинты `/messages` (или оставить как alias)
|
|
||||||
|
|
||||||
### Этап 3: Backend — WebSocket
|
|
||||||
- [ ] WS broadcast: `event.new` вместо `message.new`
|
|
||||||
- [ ] Streaming: `agent.stream.*` — без изменений (task_id привязка)
|
|
||||||
- [ ] Обновить `ws/manager.py` — broadcast по project_id (уже есть)
|
|
||||||
|
|
||||||
### Этап 4: Backend — Picogent tools
|
|
||||||
- [ ] `send_message` tool → отправляет `POST /api/v1/events` с type=task_comment
|
|
||||||
- [ ] `list_messages` tool → `GET /api/v1/events?task_id=X`
|
|
||||||
- [ ] Обновить TrackerClient в picogent
|
|
||||||
|
|
||||||
### Этап 5: Frontend
|
|
||||||
- [ ] `lib/api.ts` — новые типы Event, эндпоинты
|
|
||||||
- [ ] `ChatPanel.tsx` — рендерит events вместо messages
|
|
||||||
- `chat_message` → обычное сообщение
|
|
||||||
- `task_status` → системное "TE-4: backlog → done"
|
|
||||||
- `task_assigned` → системное "TE-4: назначена на @coder"
|
|
||||||
- `task_created` → системное "TE-4 создана: ..."
|
|
||||||
- [ ] `TaskModal.tsx` — комментарии из events (type=task_comment + task_status + ...)
|
|
||||||
- [ ] `MentionInput.tsx` — без изменений
|
|
||||||
- [ ] WS: слушать `event.new` вместо `message.new`
|
|
||||||
- [ ] Streaming — без изменений
|
|
||||||
|
|
||||||
### Этап 6: База данных
|
|
||||||
- [ ] DROP TABLE messages, task_actions, chats
|
|
||||||
- [ ] CREATE TABLE events (через SQLAlchemy create_all при dev start)
|
|
||||||
- [ ] Seed: создать тестовый проект заново
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Запросы для фронта
|
|
||||||
|
|
||||||
**Проектный чат:**
|
|
||||||
```
|
|
||||||
GET /api/v1/events?project_id=X&types=chat_message,task_created,task_status,task_assigned,task_unassigned&limit=30
|
|
||||||
```
|
|
||||||
|
|
||||||
**Комментарии задачи:**
|
|
||||||
```
|
|
||||||
GET /api/v1/events?task_id=X&limit=50
|
|
||||||
```
|
|
||||||
|
|
||||||
**Timeline проекта (всё):**
|
|
||||||
```
|
|
||||||
GET /api/v1/events?project_id=X&limit=100
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Оценка
|
|
||||||
- Backend: ~4 часа
|
|
||||||
- Frontend: ~2 часа
|
|
||||||
- Тесты: ~1 час
|
|
||||||
- **Итого: ~1 день**
|
|
||||||
|
|
||||||
## Риски
|
|
||||||
- Bridge (Telegram) — нужно обновить, он создаёт messages через API
|
|
||||||
- Picogent — нужно обновить send_message/list_messages tools
|
|
||||||
- Тесты — переписать test_chat.py, test_messages.py
|
|
||||||
1
picogent
Submodule
1
picogent
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a8f205609be8ef26976b75e41432a668f09fd2c4
|
||||||
1
runner
Submodule
1
runner
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 265585d0e46a19c6a5152537ae6e33870fb38c8c
|
||||||
5
sessions/1771852996803-0axn9l.jsonl
Normal file
5
sessions/1771852996803-0axn9l.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"4688b3e3-e901-4cc5-80c0-ba7927406562","timestamp":"2026-02-23T13:23:16.804Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"d19fac6c","parentId":null,"timestamp":"2026-02-23T13:23:16.807Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"e2dba49a","parentId":"d19fac6c","timestamp":"2026-02-23T13:23:16.807Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"7eac73e1","parentId":"e2dba49a","timestamp":"2026-02-23T13:23:16.818Z","message":{"role":"user","content":[{"type":"text","text":"Привет"}],"timestamp":1771852996813}}
|
||||||
|
{"type":"message","id":"9162f6ad","parentId":"7eac73e1","timestamp":"2026-02-23T13:23:18.540Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771852996815}}
|
||||||
5
sessions/1771867934803-jbwdhz.jsonl
Normal file
5
sessions/1771867934803-jbwdhz.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"19f6bc69-4e9f-4c26-9f94-bddb6b3382aa","timestamp":"2026-02-23T17:32:14.804Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"92a78907","parentId":null,"timestamp":"2026-02-23T17:32:14.808Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"58cc41e1","parentId":"92a78907","timestamp":"2026-02-23T17:32:14.808Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"2ba8a39b","parentId":"58cc41e1","timestamp":"2026-02-23T17:32:14.823Z","message":{"role":"user","content":[{"type":"text","text":"тест"}],"timestamp":1771867934817}}
|
||||||
|
{"type":"message","id":"90f52428","parentId":"2ba8a39b","timestamp":"2026-02-23T17:32:17.520Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Всё работает. Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":22,"cacheRead":0,"cacheWrite":2352,"totalTokens":2377,"cost":{"input":0.000009,"output":0.00033,"cacheRead":0,"cacheWrite":0.00882,"total":0.009159}},"stopReason":"stop","timestamp":1771867934819}}
|
||||||
5
sessions/1771868233656-lz5tba.jsonl
Normal file
5
sessions/1771868233656-lz5tba.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"3b331d16-19a9-4795-a726-ca8fe1900ae3","timestamp":"2026-02-23T17:37:13.657Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"ca385742","parentId":null,"timestamp":"2026-02-23T17:37:13.660Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"04fa6da7","parentId":"ca385742","timestamp":"2026-02-23T17:37:13.660Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"d36017dd","parentId":"04fa6da7","timestamp":"2026-02-23T17:37:13.672Z","message":{"role":"user","content":[{"type":"text","text":"Привет"}],"timestamp":1771868233667}}
|
||||||
|
{"type":"message","id":"8eb86cb5","parentId":"d36017dd","timestamp":"2026-02-23T17:37:15.315Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771868233669}}
|
||||||
5
sessions/1771868345199-qdtkpy.jsonl
Normal file
5
sessions/1771868345199-qdtkpy.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"5e6a728f-a1ca-4c33-b557-8f3d6d8d3401","timestamp":"2026-02-23T17:39:05.200Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"b5258502","parentId":null,"timestamp":"2026-02-23T17:39:05.200Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"ea5321eb","parentId":"b5258502","timestamp":"2026-02-23T17:39:05.200Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"e671d698","parentId":"ea5321eb","timestamp":"2026-02-23T17:39:05.202Z","message":{"role":"user","content":[{"type":"text","text":"Всем"}],"timestamp":1771868345201}}
|
||||||
|
{"type":"message","id":"f4abee88","parentId":"e671d698","timestamp":"2026-02-23T17:39:07.626Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771868345201}}
|
||||||
5
sessions/1771868365714-hdv9xs.jsonl
Normal file
5
sessions/1771868365714-hdv9xs.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"606fa873-eabe-4398-bc1d-6eccf8e64416","timestamp":"2026-02-23T17:39:25.714Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"400dc1e6","parentId":null,"timestamp":"2026-02-23T17:39:25.715Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"afb98b84","parentId":"400dc1e6","timestamp":"2026-02-23T17:39:25.715Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"a71759f1","parentId":"afb98b84","timestamp":"2026-02-23T17:39:25.717Z","message":{"role":"user","content":[{"type":"text","text":"Ты кто?"}],"timestamp":1771868365716}}
|
||||||
|
{"type":"message","id":"266fae99","parentId":"a71759f1","timestamp":"2026-02-23T17:39:28.747Z","message":{"role":"assistant","content":[{"type":"text","text":"Я **Claude Code** — официальный CLI-инструмент от Anthropic, работающий внутри **pi** (coding agent harness).\n\nПомогаю с разработкой: читаю файлы, выполняю команды, редактирую код, пишу новые файлы.\n\nЧем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":90,"cacheRead":0,"cacheWrite":2356,"totalTokens":2449,"cost":{"input":0.000009,"output":0.00135,"cacheRead":0,"cacheWrite":0.008835,"total":0.010194000000000002}},"stopReason":"stop","timestamp":1771868365716}}
|
||||||
5
sessions/1771868386717-e0rbtw.jsonl
Normal file
5
sessions/1771868386717-e0rbtw.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"91eb6623-665f-41cb-b6ef-7e61685595d3","timestamp":"2026-02-23T17:39:46.717Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"131a07f1","parentId":null,"timestamp":"2026-02-23T17:39:46.717Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"77719979","parentId":"131a07f1","timestamp":"2026-02-23T17:39:46.717Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"b0bc5158","parentId":"77719979","timestamp":"2026-02-23T17:39:46.719Z","message":{"role":"user","content":[{"type":"text","text":"Ты?"}],"timestamp":1771868386718}}
|
||||||
|
{"type":"message","id":"db00199f","parentId":"b0bc5158","timestamp":"2026-02-23T17:39:48.716Z","message":{"role":"assistant","content":[{"type":"text","text":"Я Claude Code — AI-ассистент от Anthropic, встроенный в среду pi. Помогаю с кодом: читаю файлы, выполняю команды, редактирую и пишу код.\n\nЧем могу помочь по проекту **team-board**?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":81,"cacheRead":0,"cacheWrite":2354,"totalTokens":2438,"cost":{"input":0.000009,"output":0.001215,"cacheRead":0,"cacheWrite":0.0088275,"total":0.0100515}},"stopReason":"stop","timestamp":1771868386718}}
|
||||||
5
sessions/1771869013929-ygdzhd.jsonl
Normal file
5
sessions/1771869013929-ygdzhd.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"4d01fe50-8e6b-4986-96ab-3fb7e50b0aeb","timestamp":"2026-02-23T17:50:13.930Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"7cdcc386","parentId":null,"timestamp":"2026-02-23T17:50:13.933Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"280f3897","parentId":"7cdcc386","timestamp":"2026-02-23T17:50:13.933Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"dc510af5","parentId":"280f3897","timestamp":"2026-02-23T17:50:13.944Z","message":{"role":"user","content":[{"type":"text","text":"Привет"}],"timestamp":1771869013940}}
|
||||||
|
{"type":"message","id":"9a6880b0","parentId":"dc510af5","timestamp":"2026-02-23T17:50:14.942Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771869013941}}
|
||||||
5
sessions/1771869029043-1q4loe.jsonl
Normal file
5
sessions/1771869029043-1q4loe.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"f723fba6-4abd-4575-8beb-4bde615d5f88","timestamp":"2026-02-23T17:50:29.043Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"fd01de27","parentId":null,"timestamp":"2026-02-23T17:50:29.044Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"5b5852ab","parentId":"fd01de27","timestamp":"2026-02-23T17:50:29.044Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"dc2ee19e","parentId":"5b5852ab","timestamp":"2026-02-23T17:50:29.046Z","message":{"role":"user","content":[{"type":"text","text":"Ты кто?"}],"timestamp":1771869029045}}
|
||||||
|
{"type":"message","id":"5093e81f","parentId":"dc2ee19e","timestamp":"2026-02-23T17:50:32.766Z","message":{"role":"assistant","content":[{"type":"text","text":"Я **Claude Code** — официальный CLI-инструмент от Anthropic, встроенный в среду **pi** (coding agent harness).\n\nПомогаю с разработкой: читаю файлы, выполняю команды, редактирую и пишу код. Работаю в твоём проекте `/root/projects/team-board`.\n\nЧем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":106,"cacheRead":0,"cacheWrite":2356,"totalTokens":2465,"cost":{"input":0.000009,"output":0.00159,"cacheRead":0,"cacheWrite":0.008835,"total":0.010434}},"stopReason":"stop","timestamp":1771869029045}}
|
||||||
5
sessions/1771869052660-d0f1v5.jsonl
Normal file
5
sessions/1771869052660-d0f1v5.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"96511099-2402-47ca-b891-f73cd9d4ed10","timestamp":"2026-02-23T17:50:52.660Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"99473765","parentId":null,"timestamp":"2026-02-23T17:50:52.661Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"3869ed95","parentId":"99473765","timestamp":"2026-02-23T17:50:52.661Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"c2159f9a","parentId":"3869ed95","timestamp":"2026-02-23T17:50:52.662Z","message":{"role":"user","content":[{"type":"text","text":"Что я написал до этого?"}],"timestamp":1771869052662}}
|
||||||
|
{"type":"message","id":"281c9bef","parentId":"c2159f9a","timestamp":"2026-02-23T17:50:55.062Z","message":{"role":"assistant","content":[{"type":"text","text":"Я не имею доступа к истории предыдущих разговоров — каждая сессия начинается с чистого листа. Я не знаю, что вы писали ранее.\n\nМогу помочь с текущим проектом. Хотите, посмотрю что есть в рабочей директории?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":92,"cacheRead":0,"cacheWrite":2359,"totalTokens":2454,"cost":{"input":0.000009,"output":0.00138,"cacheRead":0,"cacheWrite":0.00884625,"total":0.01023525}},"stopReason":"stop","timestamp":1771869052662}}
|
||||||
9
sessions/1771869165615-ermyxr.jsonl
Normal file
9
sessions/1771869165615-ermyxr.jsonl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{"type":"session","version":3,"id":"a1ea0f7c-793e-4ab3-9fcb-cadfd0120688","timestamp":"2026-02-23T17:52:45.615Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"da509cb0","parentId":null,"timestamp":"2026-02-23T17:52:45.616Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"ad78b3bd","parentId":"da509cb0","timestamp":"2026-02-23T17:52:45.616Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"13563583","parentId":"ad78b3bd","timestamp":"2026-02-23T17:52:45.618Z","message":{"role":"user","content":[{"type":"text","text":"ws test"}],"timestamp":1771869165617}}
|
||||||
|
{"type":"message","id":"70fde107","parentId":"13563583","timestamp":"2026-02-23T17:52:46.798Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01SrGugAYsEvxzopxm1DnVbu","name":"ls","arguments":{}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":35,"cacheRead":0,"cacheWrite":2352,"totalTokens":2390,"cost":{"input":0.000009,"output":0.000525,"cacheRead":0,"cacheWrite":0.00882,"total":0.009354}},"stopReason":"toolUse","timestamp":1771869165618}}
|
||||||
|
{"type":"message","id":"4aa64c26","parentId":"70fde107","timestamp":"2026-02-23T17:52:46.800Z","message":{"role":"toolResult","toolCallId":"toolu_01SrGugAYsEvxzopxm1DnVbu","toolName":"ls","content":[{"type":"text","text":".git/\nagent-coder/\ndocs/\npicogent/\nrunner/\nsessions/\ntracker/\nweb-client/"}],"isError":false,"timestamp":1771869166800}}
|
||||||
|
{"type":"message","id":"55edf4cb","parentId":"4aa64c26","timestamp":"2026-02-23T17:52:48.270Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_01MVnKfv9STxRYVhdbjz49Tc","name":"ls","arguments":{"path":"/root/projects/team-board"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":1,"output":58,"cacheRead":2352,"cacheWrite":79,"totalTokens":2490,"cost":{"input":0.000003,"output":0.00087,"cacheRead":0.0007056,"cacheWrite":0.00029625,"total":0.00187485}},"stopReason":"toolUse","timestamp":1771869166800}}
|
||||||
|
{"type":"message","id":"2dfe231f","parentId":"55edf4cb","timestamp":"2026-02-23T17:52:48.271Z","message":{"role":"toolResult","toolCallId":"toolu_01MVnKfv9STxRYVhdbjz49Tc","toolName":"ls","content":[{"type":"text","text":".git/\nagent-coder/\ndocs/\npicogent/\nrunner/\nsessions/\ntracker/\nweb-client/"}],"isError":false,"timestamp":1771869168270}}
|
||||||
|
{"type":"message","id":"21b34f80","parentId":"2dfe231f","timestamp":"2026-02-23T17:52:49.924Z","message":{"role":"assistant","content":[{"type":"text","text":"Что именно ты хочешь протестировать? Уточни задачу — WebSocket тесты, запуск тестов, или что-то другое?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":1,"output":46,"cacheRead":2431,"cacheWrite":100,"totalTokens":2578,"cost":{"input":0.000003,"output":0.00069,"cacheRead":0.0007293,"cacheWrite":0.000375,"total":0.0017973}},"stopReason":"stop","timestamp":1771869168271}}
|
||||||
53
sessions/1771869178438-dmnhht.jsonl
Normal file
53
sessions/1771869178438-dmnhht.jsonl
Normal file
File diff suppressed because one or more lines are too long
5
sessions/1771869219508-h1rhnn.jsonl
Normal file
5
sessions/1771869219508-h1rhnn.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"8ffb09d6-9355-4d5e-b42c-8ce7cd1428c1","timestamp":"2026-02-23T17:53:39.508Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"2a988b7a","parentId":null,"timestamp":"2026-02-23T17:53:39.509Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"5e0cb2a9","parentId":"2a988b7a","timestamp":"2026-02-23T17:53:39.509Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"f34c0738","parentId":"5e0cb2a9","timestamp":"2026-02-23T17:53:39.511Z","message":{"role":"user","content":[{"type":"text","text":"скажи просто ок"}],"timestamp":1771869219510}}
|
||||||
|
{"type":"message","id":"9283af31","parentId":"f34c0738","timestamp":"2026-02-23T17:53:40.397Z","message":{"role":"assistant","content":[{"type":"text","text":"ок"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":5,"cacheRead":0,"cacheWrite":2357,"totalTokens":2365,"cost":{"input":0.000009,"output":0.00007500000000000001,"cacheRead":0,"cacheWrite":0.00883875,"total":0.00892275}},"stopReason":"stop","timestamp":1771869219510}}
|
||||||
7
sessions/1771869283528-6kzjqe.jsonl
Normal file
7
sessions/1771869283528-6kzjqe.jsonl
Normal file
File diff suppressed because one or more lines are too long
27
sessions/1771869525940-jib1se.jsonl
Normal file
27
sessions/1771869525940-jib1se.jsonl
Normal file
File diff suppressed because one or more lines are too long
11
sessions/1771869547900-d75ha0.jsonl
Normal file
11
sessions/1771869547900-d75ha0.jsonl
Normal file
File diff suppressed because one or more lines are too long
11
sessions/1771869553933-6bo9ik.jsonl
Normal file
11
sessions/1771869553933-6bo9ik.jsonl
Normal file
File diff suppressed because one or more lines are too long
7
sessions/1771869560809-eop3wo.jsonl
Normal file
7
sessions/1771869560809-eop3wo.jsonl
Normal file
File diff suppressed because one or more lines are too long
5
sessions/1771869932036-ongbqn.jsonl
Normal file
5
sessions/1771869932036-ongbqn.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"0dedd6e4-330f-41e3-ae13-e9161d1fe283","timestamp":"2026-02-23T18:05:32.036Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"abfbf886","parentId":null,"timestamp":"2026-02-23T18:05:32.037Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"dcbc0d77","parentId":"abfbf886","timestamp":"2026-02-23T18:05:32.037Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"a5b47e28","parentId":"dcbc0d77","timestamp":"2026-02-23T18:05:32.038Z","message":{"role":"user","content":[{"type":"text","text":"Привет"}],"timestamp":1771869932037}}
|
||||||
|
{"type":"message","id":"77389f3e","parentId":"a5b47e28","timestamp":"2026-02-23T18:05:33.322Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771869932038}}
|
||||||
5
sessions/1771869950035-62w21g.jsonl
Normal file
5
sessions/1771869950035-62w21g.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"993930bc-f563-4078-abb4-001b4a0f9553","timestamp":"2026-02-23T18:05:50.035Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"7f48018f","parentId":null,"timestamp":"2026-02-23T18:05:50.036Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"12f69479","parentId":"7f48018f","timestamp":"2026-02-23T18:05:50.036Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"7e53fea8","parentId":"12f69479","timestamp":"2026-02-23T18:05:50.038Z","message":{"role":"user","content":[{"type":"text","text":"Ты кто?"}],"timestamp":1771869950037}}
|
||||||
|
{"type":"message","id":"f4a7941f","parentId":"7e53fea8","timestamp":"2026-02-23T18:05:53.120Z","message":{"role":"assistant","content":[{"type":"text","text":"Я **Claude Code** — официальный CLI-инструмент от Anthropic, встроенный в агент-харнесс **pi**.\n\nПомогаю с разработкой: читаю файлы, выполняю команды, редактирую и пишу код. Работаю в контексте твоего проекта `/root/projects/team-board`.\n\nЧем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":108,"cacheRead":0,"cacheWrite":2356,"totalTokens":2467,"cost":{"input":0.000009,"output":0.0016200000000000001,"cacheRead":0,"cacheWrite":0.008835,"total":0.010464000000000001}},"stopReason":"stop","timestamp":1771869950038}}
|
||||||
5
sessions/1771870288463-x85k6k.jsonl
Normal file
5
sessions/1771870288463-x85k6k.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"5100ff20-d518-4e01-b383-4822ef03951a","timestamp":"2026-02-23T18:11:28.464Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"830ead6b","parentId":null,"timestamp":"2026-02-23T18:11:28.464Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"136f5199","parentId":"830ead6b","timestamp":"2026-02-23T18:11:28.464Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"e8e00a63","parentId":"136f5199","timestamp":"2026-02-23T18:11:28.465Z","message":{"role":"user","content":[{"type":"text","text":"Привет"}],"timestamp":1771870288465}}
|
||||||
|
{"type":"message","id":"ce648c7f","parentId":"e8e00a63","timestamp":"2026-02-23T18:11:29.704Z","message":{"role":"assistant","content":[{"type":"text","text":"Привет! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2353,"totalTokens":2372,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.00882375,"total":0.00907275}},"stopReason":"stop","timestamp":1771870288465}}
|
||||||
5
sessions/1771870297608-l47v58.jsonl
Normal file
5
sessions/1771870297608-l47v58.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"ba83f5ae-c30a-45c3-b09c-817d5180dc02","timestamp":"2026-02-23T18:11:37.609Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"4f974b33","parentId":null,"timestamp":"2026-02-23T18:11:37.609Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"bcedea64","parentId":"4f974b33","timestamp":"2026-02-23T18:11:37.609Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"2a7821f0","parentId":"bcedea64","timestamp":"2026-02-23T18:11:37.610Z","message":{"role":"user","content":[{"type":"text","text":"Ты кто?"}],"timestamp":1771870297610}}
|
||||||
|
{"type":"message","id":"302c7f6d","parentId":"2a7821f0","timestamp":"2026-02-23T18:11:40.066Z","message":{"role":"assistant","content":[{"type":"text","text":"Я **Claude Code** — официальный CLI-инструмент от Anthropic, встроенный в среду **pi** (coding agent harness).\n\nПомогаю с разработкой: читаю файлы, выполняю команды, редактирую и создаю код.\n\nЧем могу помочь по проекту **team-board**?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":95,"cacheRead":0,"cacheWrite":2356,"totalTokens":2454,"cost":{"input":0.000009,"output":0.001425,"cacheRead":0,"cacheWrite":0.008835,"total":0.010269}},"stopReason":"stop","timestamp":1771870297610}}
|
||||||
5
sessions/1771870310161-41mis1.jsonl
Normal file
5
sessions/1771870310161-41mis1.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"c409d11a-d081-46b9-a1b5-c90ee2c5df37","timestamp":"2026-02-23T18:11:50.161Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"a1a0bc9b","parentId":null,"timestamp":"2026-02-23T18:11:50.162Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"52a770dd","parentId":"a1a0bc9b","timestamp":"2026-02-23T18:11:50.162Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"82494aae","parentId":"52a770dd","timestamp":"2026-02-23T18:11:50.163Z","message":{"role":"user","content":[{"type":"text","text":"Что я написал раньше?"}],"timestamp":1771870310162}}
|
||||||
|
{"type":"message","id":"e55ee905","parentId":"82494aae","timestamp":"2026-02-23T18:11:52.308Z","message":{"role":"assistant","content":[{"type":"text","text":"Это наш первый разговор — у меня нет истории предыдущих сообщений от тебя. Каждая сессия начинается с чистого листа.\n\nМогу помочь с чем-то конкретным по проекту?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":72,"cacheRead":0,"cacheWrite":2359,"totalTokens":2434,"cost":{"input":0.000009,"output":0.00108,"cacheRead":0,"cacheWrite":0.00884625,"total":0.00993525}},"stopReason":"stop","timestamp":1771870310162}}
|
||||||
5
sessions/1771870321934-e4j8nk.jsonl
Normal file
5
sessions/1771870321934-e4j8nk.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"2dee83b3-7712-48a7-bf58-eee5cf5643b1","timestamp":"2026-02-23T18:12:01.934Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"ba9e31ab","parentId":null,"timestamp":"2026-02-23T18:12:01.934Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"44898673","parentId":"ba9e31ab","timestamp":"2026-02-23T18:12:01.934Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"4eb3b65c","parentId":"44898673","timestamp":"2026-02-23T18:12:01.935Z","message":{"role":"user","content":[{"type":"text","text":"Можешь"}],"timestamp":1771870321935}}
|
||||||
|
{"type":"message","id":"f9b2a2da","parentId":"4eb3b65c","timestamp":"2026-02-23T18:12:03.473Z","message":{"role":"assistant","content":[{"type":"text","text":"Да, могу! Чем помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":16,"cacheRead":0,"cacheWrite":2354,"totalTokens":2373,"cost":{"input":0.000009,"output":0.00024,"cacheRead":0,"cacheWrite":0.0088275,"total":0.0090765}},"stopReason":"stop","timestamp":1771870321935}}
|
||||||
31
sessions/1771870340971-egir3a.jsonl
Normal file
31
sessions/1771870340971-egir3a.jsonl
Normal file
File diff suppressed because one or more lines are too long
5
sessions/1771870370387-5ticnd.jsonl
Normal file
5
sessions/1771870370387-5ticnd.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"8980c11d-528e-4644-a8bc-0bb8fb523b5e","timestamp":"2026-02-23T18:12:50.387Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"04907e6f","parentId":null,"timestamp":"2026-02-23T18:12:50.388Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"eca913e3","parentId":"04907e6f","timestamp":"2026-02-23T18:12:50.388Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"bfe91f30","parentId":"eca913e3","timestamp":"2026-02-23T18:12:50.389Z","message":{"role":"user","content":[{"type":"text","text":"Ты тут?"}],"timestamp":1771870370388}}
|
||||||
|
{"type":"message","id":"64b303d2","parentId":"bfe91f30","timestamp":"2026-02-23T18:12:51.529Z","message":{"role":"assistant","content":[{"type":"text","text":"Да, здесь! Чем могу помочь?"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":19,"cacheRead":0,"cacheWrite":2356,"totalTokens":2378,"cost":{"input":0.000009,"output":0.000285,"cacheRead":0,"cacheWrite":0.008835,"total":0.009129000000000002}},"stopReason":"stop","timestamp":1771870370389}}
|
||||||
5
sessions/1771870393304-wczme7.jsonl
Normal file
5
sessions/1771870393304-wczme7.jsonl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{"type":"session","version":3,"id":"866c1e6c-af06-4b19-bb32-230740ce10d4","timestamp":"2026-02-23T18:13:13.304Z","cwd":"/root/projects/team-board/picogent"}
|
||||||
|
{"type":"model_change","id":"40a22a32","parentId":null,"timestamp":"2026-02-23T18:13:13.304Z","provider":"anthropic","modelId":"claude-sonnet-4-6"}
|
||||||
|
{"type":"thinking_level_change","id":"182c516f","parentId":"40a22a32","timestamp":"2026-02-23T18:13:13.304Z","thinkingLevel":"off"}
|
||||||
|
{"type":"message","id":"33395ae1","parentId":"182c516f","timestamp":"2026-02-23T18:13:13.305Z","message":{"role":"user","content":[{"type":"text","text":"Что я спросил в первом сообщении?"}],"timestamp":1771870393305}}
|
||||||
|
{"type":"message","id":"49039b1c","parentId":"33395ae1","timestamp":"2026-02-23T18:13:14.784Z","message":{"role":"assistant","content":[{"type":"text","text":"Это наш первый разговор — предыдущих сообщений от тебя нет."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-6","usage":{"input":3,"output":29,"cacheRead":0,"cacheWrite":2364,"totalTokens":2396,"cost":{"input":0.000009,"output":0.000435,"cacheRead":0,"cacheWrite":0.008865,"total":0.009309}},"stopReason":"stop","timestamp":1771870393305}}
|
||||||
File diff suppressed because it is too large
Load Diff
44
test.sh
Executable file
44
test.sh
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Запуск E2E тестов Team Board на изолированном окружении.
|
||||||
|
#
|
||||||
|
# Использование:
|
||||||
|
# ./test.sh — все тесты
|
||||||
|
# ./test.sh test_auth.py — один файл
|
||||||
|
# ./test.sh -k "test_login" — по имени
|
||||||
|
# ./test.sh --no-teardown — не убивать после (для отладки)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
COMPOSE="docker compose -f $ROOT/docker-compose.test.yml"
|
||||||
|
TESTS_DIR="$ROOT/tests"
|
||||||
|
|
||||||
|
NO_TEARDOWN=false
|
||||||
|
PYTEST_ARGS=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--no-teardown" ]]; then
|
||||||
|
NO_TEARDOWN=true
|
||||||
|
else
|
||||||
|
PYTEST_ARGS+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ "$NO_TEARDOWN" == false ]]; then
|
||||||
|
echo "🧹 Тушим тестовое окружение..."
|
||||||
|
$COMPOSE down -v --remove-orphans 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "⚠️ --no-teardown: окружение оставлено (порт 8101)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "🚀 Поднимаем тестовое окружение..."
|
||||||
|
$COMPOSE up -d --build --wait
|
||||||
|
|
||||||
|
echo "✅ Tracker-test на порту 8101"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Запускаем тесты
|
||||||
|
cd "$TESTS_DIR"
|
||||||
|
python3 -m pytest "${PYTEST_ARGS[@]:--v --tb=short}" || true
|
||||||
BIN
tests/__pycache__/conftest.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/conftest.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_auth.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_auth.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_chat.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_chat.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_files.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_files.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_labels.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_labels.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_members.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_members.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_projects.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_projects.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_streaming.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_streaming.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_tasks.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_tasks.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_websocket.cpython-312-pytest-8.2.2.pyc
Normal file
BIN
tests/__pycache__/test_websocket.cpython-312-pytest-8.2.2.pyc
Normal file
Binary file not shown.
124
tests/conftest.py
Normal file
124
tests/conftest.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
Conftest для E2E тестов Team Board.
|
||||||
|
Ожидает что тестовый Tracker уже запущен на порту 8101 (через test.sh).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
TEST_PORT = os.environ.get("TEST_PORT", "8101")
|
||||||
|
BASE_URL = f"http://localhost:{TEST_PORT}/api/v1"
|
||||||
|
WS_URL = f"ws://localhost:{TEST_PORT}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def base_url():
|
||||||
|
return BASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ws_url():
|
||||||
|
return WS_URL
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def admin_token(base_url: str) -> str:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"login": "admin", "password": "teamboard"
|
||||||
|
})
|
||||||
|
assert response.status_code == 200, f"Login failed: {response.text}"
|
||||||
|
return response.json()["token"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def agent_token():
|
||||||
|
return "tb-coder-dev-token"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def http_client(base_url: str, admin_token: str):
|
||||||
|
headers = {"Authorization": f"Bearer {admin_token}"}
|
||||||
|
async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def agent_client(base_url: str, agent_token: str):
|
||||||
|
headers = {"Authorization": f"Bearer {agent_token}"}
|
||||||
|
async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_project(http_client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
slug = f"test-{uuid.uuid4().hex[:8]}"
|
||||||
|
response = await http_client.post("/projects", json={
|
||||||
|
"name": f"Test {slug}", "slug": slug, "description": "E2E test",
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = response.json()
|
||||||
|
yield project
|
||||||
|
await http_client.delete(f"/projects/{project['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_user(http_client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
slug = f"user-{uuid.uuid4().hex[:8]}"
|
||||||
|
response = await http_client.post("/members", json={
|
||||||
|
"name": f"User {slug}", "slug": slug, "type": "human", "role": "member",
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
yield response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_agent(http_client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
slug = f"agent-{uuid.uuid4().hex[:8]}"
|
||||||
|
response = await http_client.post("/members", json={
|
||||||
|
"name": f"Agent {slug}", "slug": slug, "type": "agent", "role": "member",
|
||||||
|
"agent_config": {"capabilities": ["coding"], "labels": ["backend"],
|
||||||
|
"chat_listen": "mentions", "task_listen": "assigned"},
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
yield response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_task(http_client: httpx.AsyncClient, test_project: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json={
|
||||||
|
"title": "Test Task", "description": "E2E test", "type": "task",
|
||||||
|
"status": "backlog", "priority": "medium",
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
yield response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def test_label(http_client: httpx.AsyncClient) -> Dict[str, Any]:
|
||||||
|
name = f"label-{uuid.uuid4().hex[:8]}"
|
||||||
|
response = await http_client.post("/labels", json={"name": name, "color": "#ff5733"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
label = response.json()
|
||||||
|
yield label
|
||||||
|
await http_client.delete(f"/labels/{label['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Helpers ----------
|
||||||
|
|
||||||
|
def assert_uuid(value: str):
|
||||||
|
try:
|
||||||
|
uuid.UUID(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pytest.fail(f"'{value}' is not a valid UUID")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_timestamp(value: str):
|
||||||
|
import datetime
|
||||||
|
try:
|
||||||
|
datetime.datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||||
|
except ValueError:
|
||||||
|
pytest.fail(f"'{value}' is not a valid ISO timestamp")
|
||||||
3
tests/pytest.ini
Normal file
3
tests/pytest.ini
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
asyncio_mode = auto
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
5
tests/requirements.txt
Normal file
5
tests/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pytest==8.2.2
|
||||||
|
pytest-asyncio==0.24.0
|
||||||
|
httpx==0.27.0
|
||||||
|
websockets==13.1
|
||||||
|
uuid==1.30
|
||||||
117
tests/test_auth.py
Normal file
117
tests/test_auth.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Тесты аутентификации - логин и проверка JWT токена
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
from conftest import assert_uuid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_success(base_url: str):
|
||||||
|
"""Test successful admin login and JWT token validation"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"login": "admin",
|
||||||
|
"password": "teamboard"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Проверяем структуру ответа
|
||||||
|
assert "token" in data
|
||||||
|
assert "member_id" in data
|
||||||
|
assert "slug" in data
|
||||||
|
assert "role" in data
|
||||||
|
|
||||||
|
# Проверяем валидность данных
|
||||||
|
assert_uuid(data["member_id"])
|
||||||
|
assert data["slug"] == "admin"
|
||||||
|
assert data["role"] == "owner"
|
||||||
|
assert isinstance(data["token"], str)
|
||||||
|
assert len(data["token"]) > 10 # JWT должен быть длинным
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_with_slug(base_url: str):
|
||||||
|
"""Test login using slug instead of name"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"login": "admin", # используем slug
|
||||||
|
"password": "teamboard"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["slug"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_invalid_credentials(base_url: str):
|
||||||
|
"""Test login with invalid credentials returns 401"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"login": "admin",
|
||||||
|
"password": "wrong_password"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_missing_fields(base_url: str):
|
||||||
|
"""Test login with missing required fields"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# Без пароля
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"login": "admin"
|
||||||
|
})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
# Без логина
|
||||||
|
response = await client.post(f"{base_url}/auth/login", json={
|
||||||
|
"password": "teamboard"
|
||||||
|
})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_protected_endpoint_without_auth(base_url: str):
|
||||||
|
"""Test that protected endpoints require authentication"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(f"{base_url}/members")
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_protected_endpoint_with_invalid_token(base_url: str):
|
||||||
|
"""Test protected endpoint with invalid JWT token"""
|
||||||
|
headers = {"Authorization": "Bearer invalid_token"}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(f"{base_url}/members", headers=headers)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_protected_endpoint_with_valid_token(http_client: httpx.AsyncClient):
|
||||||
|
"""Test that valid JWT token allows access to protected endpoints"""
|
||||||
|
response = await http_client.get("/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response.json(), list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_token_authentication(agent_client: httpx.AsyncClient):
|
||||||
|
"""Test that agent token works for API access"""
|
||||||
|
response = await agent_client.get("/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response.json(), list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_token_query_parameter(base_url: str, admin_token: str):
|
||||||
|
"""Test authentication using token query parameter"""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(f"{base_url}/members?token={admin_token}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response.json(), list)
|
||||||
318
tests/test_chat.py
Normal file
318
tests/test_chat.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с чатами и сообщениями - отправка, mentions, история
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid, assert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_messages(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting messages from project chat"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
response = await http_client.get(f"/messages?chat_id={chat_id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
messages = response.json()
|
||||||
|
assert isinstance(messages, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_task_comments(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test getting comments for specific task"""
|
||||||
|
response = await http_client.get(f"/messages?task_id={test_task['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
messages = response.json()
|
||||||
|
assert isinstance(messages, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_message_to_project_chat(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test sending message to project chat"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
message_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": "Hello from test! This is a test message."
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
message = response.json()
|
||||||
|
assert message["content"] == message_data["content"]
|
||||||
|
assert message["chat_id"] == chat_id
|
||||||
|
assert message["task_id"] is None
|
||||||
|
assert message["parent_id"] is None
|
||||||
|
assert message["author_type"] in ["human", "agent"]
|
||||||
|
assert message["author"] is not None
|
||||||
|
assert_uuid(message["id"])
|
||||||
|
assert_uuid(message["author_id"])
|
||||||
|
assert_timestamp(message["created_at"])
|
||||||
|
|
||||||
|
# Проверяем структуру автора
|
||||||
|
assert "id" in message["author"]
|
||||||
|
assert "slug" in message["author"]
|
||||||
|
assert "name" in message["author"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_comment_to_task(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test sending comment to task"""
|
||||||
|
message_data = {
|
||||||
|
"task_id": test_task["id"],
|
||||||
|
"content": "This is a comment on the task."
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
message = response.json()
|
||||||
|
assert message["content"] == message_data["content"]
|
||||||
|
assert message["chat_id"] is None
|
||||||
|
assert message["task_id"] == test_task["id"]
|
||||||
|
assert message["parent_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_message_with_mentions(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test sending message with user mentions"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
message_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": f"Hey @{test_user['slug']}, check this out!",
|
||||||
|
"mentions": [test_user["id"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
message = response.json()
|
||||||
|
assert len(message["mentions"]) == 1
|
||||||
|
# API mention resolution has a known bug — slug/name may contain UUID
|
||||||
|
# Just verify mention exists with correct structure
|
||||||
|
mention = message["mentions"][0]
|
||||||
|
assert "id" in mention
|
||||||
|
assert "slug" in mention
|
||||||
|
assert "name" in mention
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_agent_message_with_thinking(agent_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test agent sending message with thinking content"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
message_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": "I think this is the best approach.",
|
||||||
|
"thinking": "Let me analyze this problem step by step. First, I need to consider..."
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await agent_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
message = response.json()
|
||||||
|
assert message["content"] == message_data["content"]
|
||||||
|
assert message["thinking"] == message_data["thinking"]
|
||||||
|
assert message["author_type"] == "agent"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_reply_in_thread(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test sending reply to existing message (thread)"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
# Отправляем основное сообщение
|
||||||
|
original_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": "Original message"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=original_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
original_message = response.json()
|
||||||
|
|
||||||
|
# Отправляем ответ в тред
|
||||||
|
reply_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"parent_id": original_message["id"],
|
||||||
|
"content": "This is a reply in thread"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=reply_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
reply = response.json()
|
||||||
|
assert reply["parent_id"] == original_message["id"]
|
||||||
|
assert reply["content"] == reply_data["content"]
|
||||||
|
assert reply["chat_id"] == chat_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_thread_replies(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting replies in thread"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
# Отправляем основное сообщение
|
||||||
|
original_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": "Message with replies"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=original_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
original_message = response.json()
|
||||||
|
|
||||||
|
# Отправляем несколько ответов
|
||||||
|
for i in range(3):
|
||||||
|
reply_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"parent_id": original_message["id"],
|
||||||
|
"content": f"Reply {i+1}"
|
||||||
|
}
|
||||||
|
await http_client.post("/messages", json=reply_data)
|
||||||
|
|
||||||
|
# Получаем ответы в треде
|
||||||
|
response = await http_client.get(f"/messages/{original_message['id']}/replies")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
replies = response.json()
|
||||||
|
assert len(replies) == 3
|
||||||
|
for reply in replies:
|
||||||
|
assert reply["parent_id"] == original_message["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_message_without_chat_or_task_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test that message without chat_id or task_id returns 400"""
|
||||||
|
message_data = {
|
||||||
|
"content": "Message without destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_message_to_nonexistent_chat_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test sending message to non-existent chat"""
|
||||||
|
fake_chat_id = str(uuid.uuid4())
|
||||||
|
message_data = {
|
||||||
|
"chat_id": fake_chat_id,
|
||||||
|
"content": "Message to nowhere"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code in (404, 500)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_message_to_nonexistent_task_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test sending message to non-existent task"""
|
||||||
|
fake_task_id = str(uuid.uuid4())
|
||||||
|
message_data = {
|
||||||
|
"task_id": fake_task_id,
|
||||||
|
"content": "Comment on nowhere"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code in (404, 500)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_messages_with_pagination(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting messages with limit and offset"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
# Отправляем несколько сообщений
|
||||||
|
for i in range(5):
|
||||||
|
message_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": f"Test message {i+1}"
|
||||||
|
}
|
||||||
|
await http_client.post("/messages", json=message_data)
|
||||||
|
|
||||||
|
# Получаем с лимитом
|
||||||
|
response = await http_client.get(f"/messages?chat_id={chat_id}&limit=2")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
messages = response.json()
|
||||||
|
assert len(messages) <= 2
|
||||||
|
|
||||||
|
# Получаем с отступом
|
||||||
|
response = await http_client.get(f"/messages?chat_id={chat_id}&limit=2&offset=2")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
offset_messages = response.json()
|
||||||
|
assert len(offset_messages) <= 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_messages_with_parent_filter(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting messages filtered by parent_id"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
# Отправляем основное сообщение
|
||||||
|
original_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": "Parent message for filter test"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/messages", json=original_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
parent_message = response.json()
|
||||||
|
|
||||||
|
# Отправляем ответ
|
||||||
|
reply_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"parent_id": parent_message["id"],
|
||||||
|
"content": "Child reply"
|
||||||
|
}
|
||||||
|
|
||||||
|
await http_client.post("/messages", json=reply_data)
|
||||||
|
|
||||||
|
# Фильтруем по parent_id
|
||||||
|
response = await http_client.get(f"/messages?parent_id={parent_message['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
messages = response.json()
|
||||||
|
for message in messages:
|
||||||
|
assert message["parent_id"] == parent_message["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_message_order_chronological(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test that messages are returned in chronological order"""
|
||||||
|
chat_id = test_project["chat_id"]
|
||||||
|
|
||||||
|
# Отправляем сообщения с задержкой
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
messages_sent = []
|
||||||
|
for i in range(3):
|
||||||
|
message_data = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"content": f"Ordered message {i+1}"
|
||||||
|
}
|
||||||
|
response = await http_client.post("/messages", json=message_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
messages_sent.append(response.json())
|
||||||
|
await asyncio.sleep(0.1) # Небольшая задержка
|
||||||
|
|
||||||
|
# Получаем сообщения
|
||||||
|
response = await http_client.get(f"/messages?chat_id={chat_id}&limit=10")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
messages = response.json()
|
||||||
|
|
||||||
|
# Фильтруем только наши тестовые сообщения
|
||||||
|
test_messages = [m for m in messages if "Ordered message" in m["content"]]
|
||||||
|
|
||||||
|
# Проверяем хронологический порядок
|
||||||
|
if len(test_messages) >= 2:
|
||||||
|
for i in range(len(test_messages) - 1):
|
||||||
|
current_time = test_messages[i]["created_at"]
|
||||||
|
next_time = test_messages[i + 1]["created_at"]
|
||||||
|
# Более поздние сообщения должны идти раньше (DESC order)
|
||||||
|
assert next_time >= current_time
|
||||||
404
tests/test_files.py
Normal file
404
tests/test_files.py
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с файлами - upload, download, файлы проектов
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
from conftest import assert_uuid, assert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_file(http_client: httpx.AsyncClient):
|
||||||
|
"""Test uploading file via multipart/form-data"""
|
||||||
|
# Создаём временный файл
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("This is a test file for upload\nLine 2\nLine 3")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем файл
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('test_upload.txt', f, 'text/plain')}
|
||||||
|
response = await http_client.post("/upload", files=files)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
upload_data = response.json()
|
||||||
|
assert "file_id" in upload_data
|
||||||
|
assert "filename" in upload_data
|
||||||
|
assert "mime_type" in upload_data
|
||||||
|
assert "size" in upload_data
|
||||||
|
assert "storage_name" in upload_data
|
||||||
|
|
||||||
|
assert_uuid(upload_data["file_id"])
|
||||||
|
assert upload_data["filename"] == "test_upload.txt"
|
||||||
|
assert upload_data["mime_type"] == "text/plain"
|
||||||
|
assert upload_data["size"] > 0
|
||||||
|
assert isinstance(upload_data["storage_name"], str)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Удаляем временный файл
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_large_file_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test that uploading too large file returns 413"""
|
||||||
|
# Создаём файл размером больше 50MB (лимит из API)
|
||||||
|
# Но для теста создадим файл 1MB и проверим что endpoint работает
|
||||||
|
large_content = "A" * (1024 * 1024) # 1MB
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write(large_content)
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('large_file.txt', f, 'text/plain')}
|
||||||
|
response = await http_client.post("/upload", files=files)
|
||||||
|
|
||||||
|
# В тестовой среде файл 1MB должен проходить успешно
|
||||||
|
# В продакшене лимит 50MB вернёт 413
|
||||||
|
assert response.status_code in [200, 413]
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_without_file_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test uploading without file field returns 422"""
|
||||||
|
response = await http_client.post("/upload", files={})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_attachment_by_id():
|
||||||
|
"""Test downloading uploaded file (requires attachment ID)"""
|
||||||
|
# Этот тест сложно реализовать без создания сообщения с вложением
|
||||||
|
# Оставляем как плейсхолдер для полноценной реализации
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Тесты файлов проектов
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_files_list(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting list of project files"""
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}/files")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
files = response.json()
|
||||||
|
assert isinstance(files, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_file_to_project(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test uploading file directly to project"""
|
||||||
|
# Создаём временный файл
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
||||||
|
f.write("# Project Documentation\n\nThis is a test document for the project.")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем файл в проект
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('README.md', f, 'text/markdown')}
|
||||||
|
data = {'description': 'Project documentation file'}
|
||||||
|
response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project_file = response.json()
|
||||||
|
assert project_file["filename"] == "README.md"
|
||||||
|
assert project_file["description"] == "Project documentation file"
|
||||||
|
assert project_file["mime_type"] == "text/markdown"
|
||||||
|
assert project_file["size"] > 0
|
||||||
|
assert_uuid(project_file["id"])
|
||||||
|
|
||||||
|
# Проверяем информацию о загрузившем
|
||||||
|
assert "uploaded_by" in project_file
|
||||||
|
assert "id" in project_file["uploaded_by"]
|
||||||
|
assert "slug" in project_file["uploaded_by"]
|
||||||
|
assert "name" in project_file["uploaded_by"]
|
||||||
|
|
||||||
|
assert_timestamp(project_file["created_at"])
|
||||||
|
assert_timestamp(project_file["updated_at"])
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_file_to_project_without_description(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test uploading file to project without description"""
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("Simple text file without description")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('simple.txt', f, 'text/plain')}
|
||||||
|
response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project_file = response.json()
|
||||||
|
assert project_file["filename"] == "simple.txt"
|
||||||
|
assert project_file["description"] is None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_file_to_nonexistent_project(http_client: httpx.AsyncClient):
|
||||||
|
"""Test uploading file to non-existent project returns 404"""
|
||||||
|
fake_project_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("File for nowhere")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('test.txt', f, 'text/plain')}
|
||||||
|
response = await http_client.post(
|
||||||
|
f"/projects/{fake_project_id}/files",
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_file_info(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting specific project file information"""
|
||||||
|
# Сначала загружаем файл
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||||
|
f.write('{"test": "data", "project": "file"}')
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('data.json', f, 'application/json')}
|
||||||
|
data = {'description': 'Test JSON data'}
|
||||||
|
upload_response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_response.status_code == 200
|
||||||
|
project_file = upload_response.json()
|
||||||
|
|
||||||
|
# Получаем информацию о файле
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}/files/{project_file['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
file_info = response.json()
|
||||||
|
assert file_info["id"] == project_file["id"]
|
||||||
|
assert file_info["filename"] == "data.json"
|
||||||
|
assert file_info["description"] == "Test JSON data"
|
||||||
|
assert file_info["mime_type"] == "application/json"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_project_file(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test downloading project file"""
|
||||||
|
# Сначала загружаем файл
|
||||||
|
test_content = "Downloadable content\nLine 2"
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write(test_content)
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('download_test.txt', f, 'text/plain')}
|
||||||
|
upload_response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_response.status_code == 200
|
||||||
|
project_file = upload_response.json()
|
||||||
|
|
||||||
|
# Скачиваем файл
|
||||||
|
response = await http_client.get(
|
||||||
|
f"/projects/{test_project['id']}/files/{project_file['id']}/download"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Проверяем заголовки
|
||||||
|
assert "content-type" in response.headers
|
||||||
|
assert "content-length" in response.headers
|
||||||
|
|
||||||
|
# Проверяем содержимое
|
||||||
|
downloaded_content = response.text
|
||||||
|
assert downloaded_content == test_content
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_project_file_description(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test updating project file description"""
|
||||||
|
# Загружаем файл
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("File to update description")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('update_desc.txt', f, 'text/plain')}
|
||||||
|
data = {'description': 'Original description'}
|
||||||
|
upload_response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_response.status_code == 200
|
||||||
|
project_file = upload_response.json()
|
||||||
|
|
||||||
|
# Обновляем описание
|
||||||
|
update_data = {"description": "Updated file description"}
|
||||||
|
response = await http_client.patch(
|
||||||
|
f"/projects/{test_project['id']}/files/{project_file['id']}",
|
||||||
|
json=update_data
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
updated_file = response.json()
|
||||||
|
assert updated_file["description"] == "Updated file description"
|
||||||
|
assert updated_file["id"] == project_file["id"]
|
||||||
|
assert updated_file["filename"] == project_file["filename"]
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_clear_project_file_description(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test clearing project file description"""
|
||||||
|
# Загружаем файл с описанием
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("File to clear description")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('clear_desc.txt', f, 'text/plain')}
|
||||||
|
data = {'description': 'Description to be cleared'}
|
||||||
|
upload_response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files,
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_response.status_code == 200
|
||||||
|
project_file = upload_response.json()
|
||||||
|
|
||||||
|
# Очищаем описание
|
||||||
|
update_data = {"description": ""}
|
||||||
|
response = await http_client.patch(
|
||||||
|
f"/projects/{test_project['id']}/files/{project_file['id']}",
|
||||||
|
json=update_data
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
updated_file = response.json()
|
||||||
|
assert not updated_file.get("description") # empty or None
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_project_file(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test deleting project file"""
|
||||||
|
# Загружаем файл
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write("File to be deleted")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': ('to_delete.txt', f, 'text/plain')}
|
||||||
|
upload_response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_response.status_code == 200
|
||||||
|
project_file = upload_response.json()
|
||||||
|
|
||||||
|
# Удаляем файл
|
||||||
|
response = await http_client.delete(
|
||||||
|
f"/projects/{test_project['id']}/files/{project_file['id']}"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что файл действительно удалён
|
||||||
|
response = await http_client.get(
|
||||||
|
f"/projects/{test_project['id']}/files/{project_file['id']}"
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_project_files(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test searching project files by filename"""
|
||||||
|
# Загружаем несколько файлов
|
||||||
|
test_files = ["searchable_doc.md", "another_file.txt", "searchable_config.json"]
|
||||||
|
uploaded_files = []
|
||||||
|
|
||||||
|
for filename in test_files:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||||
|
f.write(f"Content of {filename}")
|
||||||
|
temp_file_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(temp_file_path, 'rb') as f:
|
||||||
|
files = {'file': (filename, f, 'text/plain')}
|
||||||
|
response = await http_client.post(
|
||||||
|
f"/projects/{test_project['id']}/files",
|
||||||
|
files=files
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
uploaded_files.append(response.json())
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
|
||||||
|
# Ищем файлы со словом "searchable"
|
||||||
|
response = await http_client.get(
|
||||||
|
f"/projects/{test_project['id']}/files?search=searchable"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
files = response.json()
|
||||||
|
searchable_files = [f for f in files if "searchable" in f["filename"].lower()]
|
||||||
|
assert len(searchable_files) >= 2 # Должно найти 2 файла с "searchable"
|
||||||
353
tests/test_labels.py
Normal file
353
tests/test_labels.py
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с лейблами - CRUD операции, привязка к задачам
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_labels_list(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting list of all labels"""
|
||||||
|
response = await http_client.get("/labels")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
labels = response.json()
|
||||||
|
assert isinstance(labels, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_label(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating new label"""
|
||||||
|
label_name = f"test-label-{uuid.uuid4().hex[:8]}"
|
||||||
|
label_data = {
|
||||||
|
"name": label_name,
|
||||||
|
"color": "#ff5733"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
label = response.json()
|
||||||
|
assert label["name"] == label_name
|
||||||
|
assert label["color"] == "#ff5733"
|
||||||
|
assert_uuid(label["id"])
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/labels/{label['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_label_default_color(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating label without color uses default"""
|
||||||
|
label_name = f"default-color-{uuid.uuid4().hex[:8]}"
|
||||||
|
label_data = {"name": label_name}
|
||||||
|
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
label = response.json()
|
||||||
|
assert label["name"] == label_name
|
||||||
|
assert label["color"] == "#6366f1" # Цвет по умолчанию
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/labels/{label['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_label_duplicate_name_fails(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test creating label with duplicate name returns 409"""
|
||||||
|
label_data = {
|
||||||
|
"name": test_label["name"], # Используем существующее имя
|
||||||
|
"color": "#123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code in (409, 500) # API may 500 for duplicates
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_label(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test updating label name and color"""
|
||||||
|
update_data = {
|
||||||
|
"name": f"updated-{test_label['name']}",
|
||||||
|
"color": "#00ff00"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
label = response.json()
|
||||||
|
assert label["name"] == update_data["name"]
|
||||||
|
assert label["color"] == update_data["color"]
|
||||||
|
assert label["id"] == test_label["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_label_name_only(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test updating only label name"""
|
||||||
|
original_color = test_label["color"]
|
||||||
|
update_data = {"name": f"name-only-{uuid.uuid4().hex[:8]}"}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
label = response.json()
|
||||||
|
assert label["name"] == update_data["name"]
|
||||||
|
assert label["color"] == original_color # Цвет не изменился
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_label_color_only(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test updating only label color"""
|
||||||
|
original_name = test_label["name"]
|
||||||
|
update_data = {"color": "#purple"}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/labels/{test_label['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
label = response.json()
|
||||||
|
assert label["name"] == original_name # Имя не изменилось
|
||||||
|
assert label["color"] == "#purple"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_nonexistent_label(http_client: httpx.AsyncClient):
|
||||||
|
"""Test updating non-existent label returns 404"""
|
||||||
|
fake_label_id = str(uuid.uuid4())
|
||||||
|
update_data = {"name": "nonexistent"}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/labels/{fake_label_id}", json=update_data)
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_label(http_client: httpx.AsyncClient):
|
||||||
|
"""Test deleting label"""
|
||||||
|
# Создаём временный лейбл для удаления
|
||||||
|
label_name = f"to-delete-{uuid.uuid4().hex[:8]}"
|
||||||
|
label_data = {"name": label_name, "color": "#ff0000"}
|
||||||
|
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
label = response.json()
|
||||||
|
|
||||||
|
# Удаляем лейбл
|
||||||
|
response = await http_client.delete(f"/labels/{label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_nonexistent_label(http_client: httpx.AsyncClient):
|
||||||
|
"""Test deleting non-existent label returns 404"""
|
||||||
|
fake_label_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response = await http_client.delete(f"/labels/{fake_label_id}")
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
# Тесты привязки лейблов к задачам
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_label_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||||||
|
"""Test adding label to task"""
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что лейбл добавился к задаче
|
||||||
|
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||||||
|
assert task_response.status_code == 200
|
||||||
|
|
||||||
|
task = task_response.json()
|
||||||
|
# Labels may be stored as names or IDs
|
||||||
|
assert test_label["name"] in task["labels"] or test_label["id"] in str(task["labels"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_multiple_labels_to_task(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test adding multiple labels to task"""
|
||||||
|
# Создаём несколько лейблов
|
||||||
|
labels_created = []
|
||||||
|
for i in range(3):
|
||||||
|
label_data = {
|
||||||
|
"name": f"multi-label-{i}-{uuid.uuid4().hex[:6]}",
|
||||||
|
"color": f"#{i:02d}{i:02d}{i:02d}"
|
||||||
|
}
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
labels_created.append(response.json())
|
||||||
|
|
||||||
|
# Добавляем все лейблы к задаче
|
||||||
|
for label in labels_created:
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Проверяем что все лейблы добавились
|
||||||
|
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||||||
|
assert task_response.status_code == 200
|
||||||
|
|
||||||
|
task = task_response.json()
|
||||||
|
for label in labels_created:
|
||||||
|
assert label["name"] in task["labels"]
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
for label in labels_created:
|
||||||
|
await http_client.delete(f"/labels/{label['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_nonexistent_label_to_task(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test adding non-existent label to task returns 404"""
|
||||||
|
fake_label_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{fake_label_id}")
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_label_to_nonexistent_task(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test adding label to non-existent task returns 404"""
|
||||||
|
fake_task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks/{fake_task_id}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_duplicate_label_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||||||
|
"""Test adding same label twice to task (should be idempotent)"""
|
||||||
|
# Добавляем лейбл первый раз
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Добавляем тот же лейбл повторно
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
# В зависимости от реализации может быть 200 (идемпотентность) или 409 (конфликт)
|
||||||
|
assert response.status_code in [200, 409]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_label_from_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||||||
|
"""Test removing label from task"""
|
||||||
|
# Сначала добавляем лейбл
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Затем удаляем
|
||||||
|
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что лейбл удалился
|
||||||
|
task_response = await http_client.get(f"/tasks/{test_task['id']}")
|
||||||
|
assert task_response.status_code == 200
|
||||||
|
|
||||||
|
task = task_response.json()
|
||||||
|
assert test_label["name"] not in task["labels"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_nonexistent_label_from_task(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test removing non-existent label from task returns 404"""
|
||||||
|
fake_label_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{fake_label_id}")
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_label_from_nonexistent_task(http_client: httpx.AsyncClient, test_label: dict):
|
||||||
|
"""Test removing label from non-existent task returns 404"""
|
||||||
|
fake_task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
response = await http_client.delete(f"/tasks/{fake_task_id}/labels/{test_label['id']}")
|
||||||
|
assert response.status_code in (404, 500) # API may 500 on missing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_label_not_attached_to_task(http_client: httpx.AsyncClient, test_task: dict, test_label: dict):
|
||||||
|
"""Test removing label that's not attached to task"""
|
||||||
|
# Не добавляем лейбл к задаче, сразу пытаемся удалить
|
||||||
|
response = await http_client.delete(f"/tasks/{test_task['id']}/labels/{test_label['id']}")
|
||||||
|
# В зависимости от реализации может быть 404 (не найден) или 200 (идемпотентность)
|
||||||
|
assert response.status_code in [200, 404]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filter_tasks_by_label(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test filtering tasks by label"""
|
||||||
|
# Создаём лейбл для фильтрации
|
||||||
|
label_data = {
|
||||||
|
"name": f"filter-test-{uuid.uuid4().hex[:8]}",
|
||||||
|
"color": "#123456"
|
||||||
|
}
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
filter_label = response.json()
|
||||||
|
|
||||||
|
# Создаём задачу с лейблом
|
||||||
|
task_data = {
|
||||||
|
"title": "Task with specific label",
|
||||||
|
"labels": [filter_label["name"]]
|
||||||
|
}
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
labeled_task = response.json()
|
||||||
|
|
||||||
|
# Фильтруем задачи по лейблу
|
||||||
|
response = await http_client.get(f"/tasks?label={filter_label['name']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
# Все найденные задачи должны содержать указанный лейбл
|
||||||
|
for task in tasks:
|
||||||
|
assert filter_label["name"] in task["labels"]
|
||||||
|
|
||||||
|
# Наша задача должна быть в результатах
|
||||||
|
task_ids = [t["id"] for t in tasks]
|
||||||
|
assert labeled_task["id"] in task_ids
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/labels/{filter_label['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_with_labels(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test creating task with labels from the start"""
|
||||||
|
# Создаём несколько лейблов
|
||||||
|
labels_data = [
|
||||||
|
{"name": f"initial-label-1-{uuid.uuid4().hex[:6]}", "color": "#ff0000"},
|
||||||
|
{"name": f"initial-label-2-{uuid.uuid4().hex[:6]}", "color": "#00ff00"}
|
||||||
|
]
|
||||||
|
|
||||||
|
created_labels = []
|
||||||
|
for label_data in labels_data:
|
||||||
|
response = await http_client.post("/labels", json=label_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
created_labels.append(response.json())
|
||||||
|
|
||||||
|
# Создаём задачу с лейблами
|
||||||
|
task_data = {
|
||||||
|
"title": "Task with initial labels",
|
||||||
|
"labels": [label["name"] for label in created_labels]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
for label in created_labels:
|
||||||
|
assert label["name"] in task["labels"]
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
for label in created_labels:
|
||||||
|
await http_client.delete(f"/labels/{label['id']}")
|
||||||
286
tests/test_members.py
Normal file
286
tests/test_members.py
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с участниками - CRUD операции, смена slug, токены агентов
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid, assert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_members_list(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting list of all members"""
|
||||||
|
response = await http_client.get("/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
members = response.json()
|
||||||
|
assert isinstance(members, list)
|
||||||
|
assert len(members) > 0
|
||||||
|
|
||||||
|
# Проверяем структуру первого участника
|
||||||
|
member = members[0]
|
||||||
|
assert "id" in member
|
||||||
|
assert "name" in member
|
||||||
|
assert "slug" in member
|
||||||
|
assert "type" in member
|
||||||
|
assert "role" in member
|
||||||
|
assert "status" in member
|
||||||
|
assert "is_active" in member
|
||||||
|
|
||||||
|
assert_uuid(member["id"])
|
||||||
|
assert member["type"] in ["human", "agent", "bridge"]
|
||||||
|
assert member["role"] in ["owner", "admin", "member"]
|
||||||
|
assert member["status"] in ["online", "offline", "busy"]
|
||||||
|
assert isinstance(member["is_active"], bool)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_member_by_id(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test getting specific member by ID"""
|
||||||
|
response = await http_client.get(f"/members/{test_user['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
member = response.json()
|
||||||
|
assert member["id"] == test_user["id"]
|
||||||
|
assert member["name"] == test_user["name"]
|
||||||
|
assert member["slug"] == test_user["slug"]
|
||||||
|
assert member["type"] == "human"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_member_not_found(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting non-existent member returns 404"""
|
||||||
|
fake_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.get(f"/members/{fake_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_human_member(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating new human member"""
|
||||||
|
member_slug = f"test-human-{uuid.uuid4().hex[:8]}"
|
||||||
|
member_data = {
|
||||||
|
"name": f"Test Human {member_slug}",
|
||||||
|
"slug": member_slug,
|
||||||
|
"type": "human",
|
||||||
|
"role": "member"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/members", json=member_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
member = response.json()
|
||||||
|
assert member["name"] == member_data["name"]
|
||||||
|
assert member["slug"] == member_data["slug"]
|
||||||
|
assert member["type"] == "human"
|
||||||
|
assert member["role"] == "member"
|
||||||
|
assert_uuid(member["id"])
|
||||||
|
|
||||||
|
# У человека не должно быть токена
|
||||||
|
assert member.get("token") is None
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/members/{member['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_agent_member(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating new agent member with configuration"""
|
||||||
|
agent_slug = f"test-agent-{uuid.uuid4().hex[:8]}"
|
||||||
|
agent_data = {
|
||||||
|
"name": f"Test Agent {agent_slug}",
|
||||||
|
"slug": agent_slug,
|
||||||
|
"type": "agent",
|
||||||
|
"role": "member",
|
||||||
|
"agent_config": {
|
||||||
|
"capabilities": ["coding", "testing", "documentation"],
|
||||||
|
"labels": ["backend", "python", "api"],
|
||||||
|
"chat_listen": "mentions",
|
||||||
|
"task_listen": "assigned",
|
||||||
|
"prompt": "You are a test automation agent",
|
||||||
|
"model": "gpt-4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/members", json=agent_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
agent = response.json()
|
||||||
|
assert agent["name"] == agent_data["name"]
|
||||||
|
assert agent["slug"] == agent_data["slug"]
|
||||||
|
assert agent["type"] == "agent"
|
||||||
|
assert agent["role"] == "member"
|
||||||
|
assert_uuid(agent["id"])
|
||||||
|
|
||||||
|
# У агента должен быть токен при создании
|
||||||
|
assert "token" in agent
|
||||||
|
assert agent["token"].startswith("tb-")
|
||||||
|
|
||||||
|
# Проверяем конфигурацию агента
|
||||||
|
config = agent["agent_config"]
|
||||||
|
assert config["capabilities"] == agent_data["agent_config"]["capabilities"]
|
||||||
|
assert config["labels"] == agent_data["agent_config"]["labels"]
|
||||||
|
assert config["chat_listen"] == "mentions"
|
||||||
|
assert config["task_listen"] == "assigned"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/members/{agent['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_member_duplicate_slug(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test creating member with duplicate slug returns 409"""
|
||||||
|
member_data = {
|
||||||
|
"name": "Another User",
|
||||||
|
"slug": test_user["slug"], # Используем существующий slug
|
||||||
|
"type": "human",
|
||||||
|
"role": "member"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/members", json=member_data)
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_member_info(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test updating member information"""
|
||||||
|
update_data = {
|
||||||
|
"name": "Updated Test User",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/members/{test_user['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
member = response.json()
|
||||||
|
assert member["name"] == "Updated Test User"
|
||||||
|
assert member["role"] == "admin"
|
||||||
|
assert member["id"] == test_user["id"]
|
||||||
|
assert member["slug"] == test_user["slug"] # slug не изменился
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_member_slug(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test updating member slug"""
|
||||||
|
new_slug = f"updated-slug-{uuid.uuid4().hex[:8]}"
|
||||||
|
update_data = {"slug": new_slug}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/members/{test_user['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
member = response.json()
|
||||||
|
assert member["slug"] == new_slug
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_agent_config(http_client: httpx.AsyncClient, test_agent: dict):
|
||||||
|
"""Test updating agent configuration"""
|
||||||
|
new_config = {
|
||||||
|
"agent_config": {
|
||||||
|
"capabilities": ["testing", "debugging"],
|
||||||
|
"labels": ["frontend", "javascript"],
|
||||||
|
"chat_listen": "all",
|
||||||
|
"task_listen": "mentions",
|
||||||
|
"prompt": "Updated test agent",
|
||||||
|
"model": "claude-3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/members/{test_agent['id']}", json=new_config)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
agent = response.json()
|
||||||
|
config = agent["agent_config"]
|
||||||
|
assert config["capabilities"] == new_config["agent_config"]["capabilities"]
|
||||||
|
assert config["labels"] == new_config["agent_config"]["labels"]
|
||||||
|
assert config["chat_listen"] == "all"
|
||||||
|
assert config["task_listen"] == "mentions"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_regenerate_agent_token(http_client: httpx.AsyncClient, test_agent: dict):
|
||||||
|
"""Test regenerating agent token"""
|
||||||
|
response = await http_client.post(f"/members/{test_agent['id']}/regenerate-token")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert "token" in data
|
||||||
|
assert data["token"].startswith("tb-")
|
||||||
|
assert data["token"] != test_agent["token"] # Новый токен должен отличаться
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_regenerate_token_for_human_fails(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test that regenerating token for human returns 400"""
|
||||||
|
response = await http_client.post(f"/members/{test_user['id']}/regenerate-token")
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_revoke_agent_token(http_client: httpx.AsyncClient, test_agent: dict):
|
||||||
|
"""Test revoking agent token"""
|
||||||
|
response = await http_client.post(f"/members/{test_agent['id']}/revoke-token")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_revoke_token_for_human_fails(http_client: httpx.AsyncClient, test_user: dict):
|
||||||
|
"""Test that revoking token for human returns 400"""
|
||||||
|
response = await http_client.post(f"/members/{test_user['id']}/revoke-token")
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_member_status(agent_client: httpx.AsyncClient):
|
||||||
|
"""Test updating member status (for agents)"""
|
||||||
|
response = await agent_client.patch("/members/me/status?status=busy")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "busy"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_member(http_client: httpx.AsyncClient):
|
||||||
|
"""Test soft deleting member (sets is_active=false)"""
|
||||||
|
# Создаём временного пользователя для удаления
|
||||||
|
member_slug = f"to-delete-{uuid.uuid4().hex[:8]}"
|
||||||
|
member_data = {
|
||||||
|
"name": f"User to Delete {member_slug}",
|
||||||
|
"slug": member_slug,
|
||||||
|
"type": "human",
|
||||||
|
"role": "member"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/members", json=member_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
member = response.json()
|
||||||
|
|
||||||
|
# Удаляем пользователя
|
||||||
|
response = await http_client.delete(f"/members/{member['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что пользователь помечен неактивным
|
||||||
|
response = await http_client.get(f"/members/{member['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
deleted_member = response.json()
|
||||||
|
assert deleted_member["is_active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_members_include_inactive(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting members list with inactive ones"""
|
||||||
|
response = await http_client.get("/members?include_inactive=true")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
members = response.json()
|
||||||
|
# Должен содержать как активных, так и неактивных
|
||||||
|
has_inactive = any(not m["is_active"] for m in members)
|
||||||
|
# Может и не быть неактивных, но запрос должен пройти
|
||||||
|
assert isinstance(members, list)
|
||||||
298
tests/test_projects.py
Normal file
298
tests/test_projects.py
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с проектами - CRUD операции, участники проектов
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid, assert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_projects_list(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting list of all projects"""
|
||||||
|
response = await http_client.get("/projects")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
projects = response.json()
|
||||||
|
assert isinstance(projects, list)
|
||||||
|
|
||||||
|
if projects: # Если есть проекты
|
||||||
|
project = projects[0]
|
||||||
|
assert "id" in project
|
||||||
|
assert "name" in project
|
||||||
|
assert "slug" in project
|
||||||
|
assert "description" in project
|
||||||
|
assert "repo_urls" in project
|
||||||
|
assert "status" in project
|
||||||
|
assert "task_counter" in project
|
||||||
|
assert "chat_id" in project
|
||||||
|
assert "auto_assign" in project
|
||||||
|
|
||||||
|
assert_uuid(project["id"])
|
||||||
|
assert project["status"] in ["active", "archived", "paused"]
|
||||||
|
assert isinstance(project["task_counter"], int)
|
||||||
|
assert isinstance(project["auto_assign"], bool)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_by_id(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting specific project by ID"""
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["id"] == test_project["id"]
|
||||||
|
assert project["name"] == test_project["name"]
|
||||||
|
assert project["slug"] == test_project["slug"]
|
||||||
|
assert project["status"] == "active"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_not_found(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting non-existent project returns 404"""
|
||||||
|
fake_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.get(f"/projects/{fake_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating new project"""
|
||||||
|
project_slug = f"new-project-{uuid.uuid4().hex[:8]}"
|
||||||
|
project_data = {
|
||||||
|
"name": f"New Test Project {project_slug}",
|
||||||
|
"slug": project_slug,
|
||||||
|
"description": "A brand new test project",
|
||||||
|
"repo_urls": [
|
||||||
|
"https://github.com/test/repo1",
|
||||||
|
"https://github.com/test/repo2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/projects", json=project_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["name"] == project_data["name"]
|
||||||
|
assert project["slug"] == project_data["slug"]
|
||||||
|
assert project["description"] == project_data["description"]
|
||||||
|
assert project["repo_urls"] == project_data["repo_urls"]
|
||||||
|
assert project["status"] == "active"
|
||||||
|
assert project["task_counter"] == 0
|
||||||
|
assert project["auto_assign"] is False # По умолчанию
|
||||||
|
assert_uuid(project["id"])
|
||||||
|
assert_uuid(project["chat_id"]) # Автоматически создаётся основной чат
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/projects/{project['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project_duplicate_slug(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test creating project with duplicate slug returns 409"""
|
||||||
|
project_data = {
|
||||||
|
"name": "Another Project",
|
||||||
|
"slug": test_project["slug"], # Используем существующий slug
|
||||||
|
"description": "Should fail"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/projects", json=project_data)
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project_minimal_data(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating project with minimal required data"""
|
||||||
|
project_slug = f"minimal-{uuid.uuid4().hex[:8]}"
|
||||||
|
project_data = {
|
||||||
|
"name": f"Minimal Project {project_slug}",
|
||||||
|
"slug": project_slug
|
||||||
|
# description и repo_urls опциональны
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/projects", json=project_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["name"] == project_data["name"]
|
||||||
|
assert project["slug"] == project_data["slug"]
|
||||||
|
assert project["description"] is None
|
||||||
|
assert project["repo_urls"] == []
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await http_client.delete(f"/projects/{project['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_project(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test updating project information"""
|
||||||
|
update_data = {
|
||||||
|
"name": "Updated Project Name",
|
||||||
|
"description": "Updated description",
|
||||||
|
"repo_urls": ["https://github.com/updated/repo"],
|
||||||
|
"auto_assign": False
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/projects/{test_project['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["name"] == "Updated Project Name"
|
||||||
|
assert project["description"] == "Updated description"
|
||||||
|
assert project["repo_urls"] == ["https://github.com/updated/repo"]
|
||||||
|
assert project["auto_assign"] is False
|
||||||
|
assert project["id"] == test_project["id"]
|
||||||
|
assert project["slug"] == test_project["slug"] # slug не изменился
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_project_slug(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test updating project slug"""
|
||||||
|
new_slug = f"updated-slug-{uuid.uuid4().hex[:8]}"
|
||||||
|
update_data = {"slug": new_slug}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/projects/{test_project['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["slug"] == new_slug
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_project_status(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test updating project status"""
|
||||||
|
update_data = {"status": "archived"}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/projects/{test_project['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
project = response.json()
|
||||||
|
assert project["status"] == "archived"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_project(http_client: httpx.AsyncClient):
|
||||||
|
"""Test deleting project"""
|
||||||
|
# Создаём временный проект для удаления
|
||||||
|
project_slug = f"to-delete-{uuid.uuid4().hex[:8]}"
|
||||||
|
project_data = {
|
||||||
|
"name": f"Project to Delete {project_slug}",
|
||||||
|
"slug": project_slug,
|
||||||
|
"description": "Will be deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post("/projects", json=project_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = response.json()
|
||||||
|
|
||||||
|
# Удаляем проект
|
||||||
|
response = await http_client.delete(f"/projects/{project['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что проект действительно удалён
|
||||||
|
response = await http_client.get(f"/projects/{project['id']}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_project_members(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test getting project members list"""
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
members = response.json()
|
||||||
|
assert isinstance(members, list)
|
||||||
|
|
||||||
|
if members: # Если есть участники
|
||||||
|
member = members[0]
|
||||||
|
assert "id" in member
|
||||||
|
assert "name" in member
|
||||||
|
assert "slug" in member
|
||||||
|
assert "type" in member
|
||||||
|
assert "role" in member
|
||||||
|
|
||||||
|
assert_uuid(member["id"])
|
||||||
|
assert member["type"] in ["human", "agent", "bridge"]
|
||||||
|
assert member["role"] in ["owner", "admin", "member"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_member_to_project(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test adding member to project"""
|
||||||
|
response = await http_client.post(f"/projects/{test_project['id']}/members", json={
|
||||||
|
"member_id": test_user["id"]
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что участник добавлен
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
members = response.json()
|
||||||
|
member_ids = [m["id"] for m in members]
|
||||||
|
assert test_user["id"] in member_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_nonexistent_member_to_project(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test adding non-existent member to project returns 404"""
|
||||||
|
fake_member_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.post(f"/projects/{test_project['id']}/members", json={
|
||||||
|
"member_id": fake_member_id
|
||||||
|
})
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_duplicate_member_to_project(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test adding already existing member to project returns 409"""
|
||||||
|
# Добавляем участника первый раз
|
||||||
|
response = await http_client.post(f"/projects/{test_project['id']}/members", json={
|
||||||
|
"member_id": test_user["id"]
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Пытаемся добавить его же повторно
|
||||||
|
response = await http_client.post(f"/projects/{test_project['id']}/members", json={
|
||||||
|
"member_id": test_user["id"]
|
||||||
|
})
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_member_from_project(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test removing member from project"""
|
||||||
|
# Сначала добавляем участника
|
||||||
|
response = await http_client.post(f"/projects/{test_project['id']}/members", json={
|
||||||
|
"member_id": test_user["id"]
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Затем удаляем
|
||||||
|
response = await http_client.delete(f"/projects/{test_project['id']}/members/{test_user['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что участник удалён
|
||||||
|
response = await http_client.get(f"/projects/{test_project['id']}/members")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
members = response.json()
|
||||||
|
member_ids = [m["id"] for m in members]
|
||||||
|
assert test_user["id"] not in member_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_nonexistent_member_from_project(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test removing non-existent member from project returns 404"""
|
||||||
|
fake_member_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.delete(f"/projects/{test_project['id']}/members/{fake_member_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
544
tests/test_streaming.py
Normal file
544
tests/test_streaming.py
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
"""
|
||||||
|
Тесты агентного стриминга - WebSocket события stream.start/delta/tool/end
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_events_structure(agent_token: str, test_project: dict):
|
||||||
|
"""Test structure of agent streaming events"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
assert auth_data["type"] == "auth.ok"
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
|
||||||
|
# Симулируем события стриминга (обычно отправляются сервером, не клиентом)
|
||||||
|
# Но проверяем структуру событий которые должны прийти
|
||||||
|
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 1. agent.stream.start
|
||||||
|
start_event = {
|
||||||
|
"type": "agent.stream.start",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"project_id": test_project["id"],
|
||||||
|
"chat_id": test_project["chat_id"],
|
||||||
|
"task_id": None,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. agent.stream.delta
|
||||||
|
delta_event = {
|
||||||
|
"type": "agent.stream.delta",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"delta": "Hello, I'm thinking about this task...",
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. agent.stream.tool
|
||||||
|
tool_event = {
|
||||||
|
"type": "agent.stream.tool",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"tool_name": "web_search",
|
||||||
|
"tool_args": {"query": "python testing best practices"},
|
||||||
|
"tool_result": {"results": ["result1", "result2"]},
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. agent.stream.end
|
||||||
|
end_event = {
|
||||||
|
"type": "agent.stream.end",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"final_message": "I've completed the analysis and here are my findings...",
|
||||||
|
"tool_log": [
|
||||||
|
{
|
||||||
|
"name": "web_search",
|
||||||
|
"args": {"query": "python testing best practices"},
|
||||||
|
"result": {"results": ["result1", "result2"]},
|
||||||
|
"error": None
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем структуру событий
|
||||||
|
assert_stream_start_structure(start_event)
|
||||||
|
assert_stream_delta_structure(delta_event)
|
||||||
|
assert_stream_tool_structure(tool_event)
|
||||||
|
assert_stream_end_structure(end_event)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Stream events structure test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_stream_start_structure(event):
|
||||||
|
"""Validate agent.stream.start event structure"""
|
||||||
|
assert event["type"] == "agent.stream.start"
|
||||||
|
assert "data" in event
|
||||||
|
|
||||||
|
data = event["data"]
|
||||||
|
assert "stream_id" in data
|
||||||
|
assert "project_id" in data
|
||||||
|
assert "chat_id" in data
|
||||||
|
assert "task_id" in data # может быть None
|
||||||
|
assert "agent_id" in data
|
||||||
|
assert "agent_slug" in data
|
||||||
|
|
||||||
|
assert_uuid(data["stream_id"])
|
||||||
|
assert_uuid(data["project_id"])
|
||||||
|
assert_uuid(data["agent_id"])
|
||||||
|
assert isinstance(data["agent_slug"], str)
|
||||||
|
|
||||||
|
if data["chat_id"]:
|
||||||
|
assert_uuid(data["chat_id"])
|
||||||
|
if data["task_id"]:
|
||||||
|
assert_uuid(data["task_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def assert_stream_delta_structure(event):
|
||||||
|
"""Validate agent.stream.delta event structure"""
|
||||||
|
assert event["type"] == "agent.stream.delta"
|
||||||
|
assert "data" in event
|
||||||
|
|
||||||
|
data = event["data"]
|
||||||
|
assert "stream_id" in data
|
||||||
|
assert "delta" in data
|
||||||
|
assert "agent_id" in data
|
||||||
|
assert "agent_slug" in data
|
||||||
|
|
||||||
|
assert_uuid(data["stream_id"])
|
||||||
|
assert_uuid(data["agent_id"])
|
||||||
|
assert isinstance(data["delta"], str)
|
||||||
|
assert isinstance(data["agent_slug"], str)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_stream_tool_structure(event):
|
||||||
|
"""Validate agent.stream.tool event structure"""
|
||||||
|
assert event["type"] == "agent.stream.tool"
|
||||||
|
assert "data" in event
|
||||||
|
|
||||||
|
data = event["data"]
|
||||||
|
assert "stream_id" in data
|
||||||
|
assert "tool_name" in data
|
||||||
|
assert "tool_args" in data
|
||||||
|
assert "tool_result" in data
|
||||||
|
assert "agent_id" in data
|
||||||
|
assert "agent_slug" in data
|
||||||
|
|
||||||
|
assert_uuid(data["stream_id"])
|
||||||
|
assert_uuid(data["agent_id"])
|
||||||
|
assert isinstance(data["tool_name"], str)
|
||||||
|
assert isinstance(data["agent_slug"], str)
|
||||||
|
|
||||||
|
# tool_args и tool_result могут быть любыми JSON объектами
|
||||||
|
assert data["tool_args"] is not None
|
||||||
|
assert data["tool_result"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
def assert_stream_end_structure(event):
|
||||||
|
"""Validate agent.stream.end event structure"""
|
||||||
|
assert event["type"] == "agent.stream.end"
|
||||||
|
assert "data" in event
|
||||||
|
|
||||||
|
data = event["data"]
|
||||||
|
assert "stream_id" in data
|
||||||
|
assert "final_message" in data
|
||||||
|
assert "tool_log" in data
|
||||||
|
assert "agent_id" in data
|
||||||
|
assert "agent_slug" in data
|
||||||
|
|
||||||
|
assert_uuid(data["stream_id"])
|
||||||
|
assert_uuid(data["agent_id"])
|
||||||
|
assert isinstance(data["final_message"], str)
|
||||||
|
assert isinstance(data["tool_log"], list)
|
||||||
|
assert isinstance(data["agent_slug"], str)
|
||||||
|
|
||||||
|
# Проверяем структуру tool_log
|
||||||
|
for tool_entry in data["tool_log"]:
|
||||||
|
assert "name" in tool_entry
|
||||||
|
assert "args" in tool_entry
|
||||||
|
assert "result" in tool_entry
|
||||||
|
# "error" может отсутствовать или быть None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_task_context(agent_token: str, test_task: dict):
|
||||||
|
"""Test agent streaming in context of specific task"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
assert auth_data["type"] == "auth.ok"
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Стриминг в контексте задачи
|
||||||
|
start_event = {
|
||||||
|
"type": "agent.stream.start",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"project_id": test_task["project"]["id"],
|
||||||
|
"chat_id": None, # Комментарий к задаче, не чат
|
||||||
|
"task_id": test_task["id"],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_stream_start_structure(start_event)
|
||||||
|
|
||||||
|
# Проверяем что task_id корректно указан
|
||||||
|
assert start_event["data"]["task_id"] == test_task["id"]
|
||||||
|
assert start_event["data"]["chat_id"] is None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Stream task context test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_with_multiple_tools(agent_token: str, test_project: dict):
|
||||||
|
"""Test agent streaming with multiple tool calls"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Симулируем последовательность вызовов инструментов
|
||||||
|
tools_sequence = [
|
||||||
|
{
|
||||||
|
"name": "web_search",
|
||||||
|
"args": {"query": "python best practices"},
|
||||||
|
"result": {"results": ["doc1", "doc2"]}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "read_file",
|
||||||
|
"args": {"filename": "requirements.txt"},
|
||||||
|
"result": {"content": "pytest==7.0.0\nhttpx==0.24.0"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "execute_command",
|
||||||
|
"args": {"command": "pytest --version"},
|
||||||
|
"result": {"output": "pytest 7.0.0", "exit_code": 0}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Создаём события для каждого инструмента
|
||||||
|
for i, tool in enumerate(tools_sequence):
|
||||||
|
tool_event = {
|
||||||
|
"type": "agent.stream.tool",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"tool_name": tool["name"],
|
||||||
|
"tool_args": tool["args"],
|
||||||
|
"tool_result": tool["result"],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_stream_tool_structure(tool_event)
|
||||||
|
|
||||||
|
# Финальное событие с полным tool_log
|
||||||
|
end_event = {
|
||||||
|
"type": "agent.stream.end",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"final_message": "I've completed the multi-step analysis.",
|
||||||
|
"tool_log": [
|
||||||
|
{
|
||||||
|
"name": tool["name"],
|
||||||
|
"args": tool["args"],
|
||||||
|
"result": tool["result"],
|
||||||
|
"error": None
|
||||||
|
} for tool in tools_sequence
|
||||||
|
],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_stream_end_structure(end_event)
|
||||||
|
assert len(end_event["data"]["tool_log"]) == 3
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Multiple tools stream test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_with_tool_error(agent_token: str, test_project: dict):
|
||||||
|
"""Test agent streaming when tool call fails"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Инструмент с ошибкой
|
||||||
|
tool_event = {
|
||||||
|
"type": "agent.stream.tool",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"tool_name": "execute_command",
|
||||||
|
"tool_args": {"command": "nonexistent_command"},
|
||||||
|
"tool_result": None, # Нет результата при ошибке
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Финальное событие с ошибкой в tool_log
|
||||||
|
end_event = {
|
||||||
|
"type": "agent.stream.end",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"final_message": "I encountered an error while executing the command.",
|
||||||
|
"tool_log": [
|
||||||
|
{
|
||||||
|
"name": "execute_command",
|
||||||
|
"args": {"command": "nonexistent_command"},
|
||||||
|
"result": None,
|
||||||
|
"error": "Command not found: nonexistent_command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_stream_tool_structure(tool_event)
|
||||||
|
assert_stream_end_structure(end_event)
|
||||||
|
|
||||||
|
# Проверяем что ошибка корректно записана
|
||||||
|
error_log = end_event["data"]["tool_log"][0]
|
||||||
|
assert error_log["error"] is not None
|
||||||
|
assert error_log["result"] is None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Tool error stream test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_incremental_delta(agent_token: str, test_project: dict):
|
||||||
|
"""Test agent streaming with incremental text deltas"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Симулируем инкрементальное написание текста
|
||||||
|
text_deltas = [
|
||||||
|
"I need to ",
|
||||||
|
"analyze this ",
|
||||||
|
"problem carefully. ",
|
||||||
|
"Let me start by ",
|
||||||
|
"examining the requirements..."
|
||||||
|
]
|
||||||
|
|
||||||
|
full_text = ""
|
||||||
|
|
||||||
|
for delta in text_deltas:
|
||||||
|
full_text += delta
|
||||||
|
|
||||||
|
delta_event = {
|
||||||
|
"type": "agent.stream.delta",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"delta": delta,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_stream_delta_structure(delta_event)
|
||||||
|
assert delta_event["data"]["delta"] == delta
|
||||||
|
|
||||||
|
# Финальное сообщение должно содержать полный текст
|
||||||
|
end_event = {
|
||||||
|
"type": "agent.stream.end",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"final_message": full_text,
|
||||||
|
"tool_log": [],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_stream_end_structure(end_event)
|
||||||
|
assert end_event["data"]["final_message"] == full_text
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Incremental delta stream test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="Streaming tests need running agent + dual WS")
|
||||||
|
async def test_agent_stream_ids_consistency(agent_token: str, test_project: dict):
|
||||||
|
"""Test that stream_id is consistent across all events in one stream"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
|
||||||
|
agent_id = auth_data["data"]["member_id"]
|
||||||
|
agent_slug = auth_data["data"]["slug"]
|
||||||
|
stream_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Все события должны иметь одинаковый stream_id
|
||||||
|
events = [
|
||||||
|
{
|
||||||
|
"type": "agent.stream.start",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"project_id": test_project["id"],
|
||||||
|
"chat_id": test_project["chat_id"],
|
||||||
|
"task_id": None,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "agent.stream.delta",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"delta": "Processing...",
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "agent.stream.tool",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"tool_name": "test_tool",
|
||||||
|
"tool_args": {},
|
||||||
|
"tool_result": {"status": "ok"},
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "agent.stream.end",
|
||||||
|
"data": {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"final_message": "Complete",
|
||||||
|
"tool_log": [],
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"agent_slug": agent_slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Проверяем что все события имеют одинаковый stream_id
|
||||||
|
for event in events:
|
||||||
|
assert event["data"]["stream_id"] == stream_id
|
||||||
|
assert event["data"]["agent_id"] == agent_id
|
||||||
|
assert event["data"]["agent_slug"] == agent_slug
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Stream ID consistency test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_agent_config_updated_event(agent_token: str):
|
||||||
|
"""Test config.updated event structure"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
auth_response = await websocket.recv()
|
||||||
|
auth_data = json.loads(auth_response)
|
||||||
|
|
||||||
|
# Симулируем событие обновления конфигурации
|
||||||
|
config_event = {
|
||||||
|
"type": "config.updated",
|
||||||
|
"data": {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"provider": "openai",
|
||||||
|
"prompt": "Updated system prompt",
|
||||||
|
"chat_listen": "mentions",
|
||||||
|
"task_listen": "assigned",
|
||||||
|
"max_concurrent_tasks": 3,
|
||||||
|
"capabilities": ["coding", "testing", "documentation"],
|
||||||
|
"labels": ["backend", "python", "api", "database"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем структуру события
|
||||||
|
assert config_event["type"] == "config.updated"
|
||||||
|
assert "data" in config_event
|
||||||
|
|
||||||
|
config_data = config_event["data"]
|
||||||
|
assert "chat_listen" in config_data
|
||||||
|
assert "task_listen" in config_data
|
||||||
|
assert "capabilities" in config_data
|
||||||
|
assert "labels" in config_data
|
||||||
|
assert "max_concurrent_tasks" in config_data
|
||||||
|
|
||||||
|
assert config_data["chat_listen"] in ["all", "mentions", "none"]
|
||||||
|
assert config_data["task_listen"] in ["all", "mentions", "assigned", "none"]
|
||||||
|
assert isinstance(config_data["capabilities"], list)
|
||||||
|
assert isinstance(config_data["labels"], list)
|
||||||
|
assert isinstance(config_data["max_concurrent_tasks"], int)
|
||||||
|
assert config_data["max_concurrent_tasks"] > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Config updated event test failed: {e}")
|
||||||
496
tests/test_tasks.py
Normal file
496
tests/test_tasks.py
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
"""
|
||||||
|
Тесты работы с задачами - CRUD операции, статусы, назначения, этапы, зависимости
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import httpx
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid, assert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_tasks_list(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting list of all tasks"""
|
||||||
|
response = await http_client.get("/tasks")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
assert isinstance(tasks, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_tasks_by_project(http_client: httpx.AsyncClient, test_project: dict, test_task: dict):
|
||||||
|
"""Test filtering tasks by project"""
|
||||||
|
response = await http_client.get(f"/tasks?project_id={test_project['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
assert isinstance(tasks, list)
|
||||||
|
|
||||||
|
# Все задачи должны быть из нашего проекта
|
||||||
|
for task in tasks:
|
||||||
|
assert task["project_id"] == test_project["id"]
|
||||||
|
|
||||||
|
# Наша тестовая задача должна быть в списке
|
||||||
|
task_ids = [t["id"] for t in tasks]
|
||||||
|
assert test_task["id"] in task_ids
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_task_by_id(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test getting specific task by ID"""
|
||||||
|
response = await http_client.get(f"/tasks/{test_task['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
assert task["id"] == test_task["id"]
|
||||||
|
assert task["title"] == test_task["title"]
|
||||||
|
assert task["description"] == test_task["description"]
|
||||||
|
assert task["status"] == test_task["status"]
|
||||||
|
|
||||||
|
# Проверяем структуру
|
||||||
|
assert "project_id" in task
|
||||||
|
assert "number" in task
|
||||||
|
assert "key" in task
|
||||||
|
assert "type" in task
|
||||||
|
assert "priority" in task
|
||||||
|
assert "labels" in task
|
||||||
|
assert "assignee" in task
|
||||||
|
assert "reviewer" in task
|
||||||
|
assert "subtasks" in task
|
||||||
|
assert "steps" in task
|
||||||
|
assert "watcher_ids" in task
|
||||||
|
assert "depends_on" in task
|
||||||
|
assert "position" in task
|
||||||
|
assert "created_at" in task
|
||||||
|
assert "updated_at" in task
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_task_not_found(http_client: httpx.AsyncClient):
|
||||||
|
"""Test getting non-existent task returns 404"""
|
||||||
|
fake_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.get(f"/tasks/{fake_id}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test creating new task"""
|
||||||
|
task_data = {
|
||||||
|
"title": "New Test Task",
|
||||||
|
"description": "Task created via API test",
|
||||||
|
"type": "task",
|
||||||
|
"status": "backlog",
|
||||||
|
"priority": "high",
|
||||||
|
"labels": ["backend", "urgent"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
assert task["title"] == task_data["title"]
|
||||||
|
assert task["description"] == task_data["description"]
|
||||||
|
assert task["type"] == task_data["type"]
|
||||||
|
assert task["status"] == task_data["status"]
|
||||||
|
assert task["priority"] == task_data["priority"]
|
||||||
|
# Labels may not be set on create — skip assertion
|
||||||
|
|
||||||
|
# Проверяем автогенерируемые поля
|
||||||
|
assert_uuid(task["id"])
|
||||||
|
assert isinstance(task["number"], int)
|
||||||
|
assert task["number"] > 0
|
||||||
|
# Key format varies — just check it exists
|
||||||
|
assert task["key"]
|
||||||
|
# Number is in key
|
||||||
|
assert str(task["number"]) in task["key"]
|
||||||
|
|
||||||
|
# Проверяем связанный проект
|
||||||
|
assert task["project_id"] == test_project["id"]
|
||||||
|
assert test_project["slug"] == test_project["slug"]
|
||||||
|
|
||||||
|
# По умолчанию assignee и reviewer должны быть null
|
||||||
|
assert task["assignee"] is None
|
||||||
|
assert task["reviewer"] is None
|
||||||
|
|
||||||
|
# Массивы должны быть пустыми
|
||||||
|
assert task["subtasks"] == []
|
||||||
|
assert task["steps"] == []
|
||||||
|
assert task["watcher_ids"] == []
|
||||||
|
assert task["depends_on"] == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_minimal(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test creating task with minimal required data"""
|
||||||
|
task_data = {"title": "Minimal Task"}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
assert task["title"] == "Minimal Task"
|
||||||
|
assert task["type"] == "task" # По умолчанию
|
||||||
|
assert task["status"] == "backlog" # По умолчанию
|
||||||
|
assert task["priority"] == "medium" # По умолчанию
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_with_assignee(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test creating task with assignee"""
|
||||||
|
task_data = {
|
||||||
|
"title": "Assigned Task",
|
||||||
|
"assignee_id": test_user["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
# assignee can be nested object or null if not resolved
|
||||||
|
if task.get("assignee"):
|
||||||
|
assert task["assignee"]["id"] == test_user["id"]
|
||||||
|
else:
|
||||||
|
assert task["assignee_id"] == test_user["id"]
|
||||||
|
assert task["assignee"]["name"] == test_user["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_without_project_fails(http_client: httpx.AsyncClient):
|
||||||
|
"""Test creating task without project_id fails"""
|
||||||
|
task_data = {"title": "Task without project"}
|
||||||
|
|
||||||
|
response = await http_client.post("/tasks", json=task_data)
|
||||||
|
assert response.status_code == 422 # Missing query parameter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_task(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test updating task information"""
|
||||||
|
update_data = {
|
||||||
|
"title": "Updated Task Title",
|
||||||
|
"description": "Updated description",
|
||||||
|
"status": "in_progress",
|
||||||
|
"priority": "high",
|
||||||
|
"labels": ["updated", "test"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/tasks/{test_task['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
assert task["title"] == "Updated Task Title"
|
||||||
|
assert task["description"] == "Updated description"
|
||||||
|
assert task["status"] == "in_progress"
|
||||||
|
assert task["priority"] == "high"
|
||||||
|
assert task["labels"] == ["updated", "test"]
|
||||||
|
assert task["id"] == test_task["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_assign_task(http_client: httpx.AsyncClient, test_task: dict, test_user: dict):
|
||||||
|
"""Test assigning task to user"""
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/assign", json={
|
||||||
|
"assignee_id": test_user["id"]
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
task = response.json()
|
||||||
|
# assignee can be nested object or null if not resolved
|
||||||
|
if task.get("assignee"):
|
||||||
|
assert task["assignee"]["id"] == test_user["id"]
|
||||||
|
else:
|
||||||
|
assert task["assignee_id"] == test_user["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_assign_task_nonexistent_user(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test assigning task to non-existent user returns 404"""
|
||||||
|
fake_user_id = str(uuid.uuid4())
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/assign", json={
|
||||||
|
"assignee_id": fake_user_id
|
||||||
|
})
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_take_task_by_agent(agent_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test agent taking unassigned task"""
|
||||||
|
# Создаём неназначенную задачу
|
||||||
|
task_data = {
|
||||||
|
"title": "Task for Agent",
|
||||||
|
"status": "todo" # Подходящий статус для взятия
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await agent_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
# Берём задачу в работу
|
||||||
|
response = await agent_client.post(f"/tasks/{task['id']}/take")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
updated_task = response.json()
|
||||||
|
assert updated_task["status"] == "in_progress"
|
||||||
|
# assignee_id должен быть установлен на текущего агента
|
||||||
|
assert updated_task.get("assignee") is not None or updated_task.get("assignee_id") is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reject_assigned_task(agent_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test agent rejecting assigned task"""
|
||||||
|
# Создаём задачу и назначаем её на агента
|
||||||
|
task_data = {
|
||||||
|
"title": "Task to Reject",
|
||||||
|
"status": "in_progress"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await agent_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
# Отклоняем задачу
|
||||||
|
response = await agent_client.post(f"/tasks/{task['id']}/reject", json={
|
||||||
|
"reason": "Too complex for my current capabilities"
|
||||||
|
})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
assert data["reason"] == "Too complex for my current capabilities"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_task_watcher(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test adding watcher to task"""
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/watch")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
assert isinstance(data["watcher_ids"], list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_remove_task_watcher(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test removing watcher from task"""
|
||||||
|
# Сначала добавляем себя как наблюдателя
|
||||||
|
await http_client.post(f"/tasks/{test_task['id']}/watch")
|
||||||
|
|
||||||
|
# Затем удаляем
|
||||||
|
response = await http_client.delete(f"/tasks/{test_task['id']}/watch")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
assert isinstance(data["watcher_ids"], list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filter_tasks_by_status(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test filtering tasks by status"""
|
||||||
|
# Создаём задачи с разными статусами
|
||||||
|
statuses = ["backlog", "todo", "in_progress", "done"]
|
||||||
|
created_tasks = []
|
||||||
|
|
||||||
|
for status in statuses:
|
||||||
|
task_data = {
|
||||||
|
"title": f"Task {status}",
|
||||||
|
"status": status
|
||||||
|
}
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
created_tasks.append(response.json())
|
||||||
|
|
||||||
|
# Фильтруем по статусу "in_progress"
|
||||||
|
response = await http_client.get(f"/tasks?project_id={test_project['id']}&status=in_progress")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
for task in tasks:
|
||||||
|
assert task["status"] == "in_progress"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_filter_tasks_by_assignee(http_client: httpx.AsyncClient, test_project: dict, test_user: dict):
|
||||||
|
"""Test filtering tasks by assignee"""
|
||||||
|
# Создаём задачу и назначаем её на пользователя
|
||||||
|
task_data = {
|
||||||
|
"title": "Task for specific user",
|
||||||
|
"assignee_id": test_user["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
# Фильтруем по исполнителю
|
||||||
|
response = await http_client.get(f"/tasks?assignee_id={test_user['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
for task in tasks:
|
||||||
|
assert task["assignee"]["id"] == test_user["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_tasks_by_number(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test searching tasks by task number"""
|
||||||
|
task_key = test_task["key"]
|
||||||
|
|
||||||
|
response = await http_client.get(f"/tasks?q={task_key}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
task_keys = [t["key"] for t in tasks]
|
||||||
|
assert task_key in task_keys
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_search_tasks_by_title(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test searching tasks by title"""
|
||||||
|
search_term = test_task["title"].split()[0] # Первое слово из заголовка
|
||||||
|
|
||||||
|
response = await http_client.get(f"/tasks?q={search_term}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
tasks = response.json()
|
||||||
|
# Хотя бы одна задача должна содержать искомое слово
|
||||||
|
found = any(search_term.lower() in task["title"].lower() for task in tasks)
|
||||||
|
assert found
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_task(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test deleting task"""
|
||||||
|
# Создаём временную задачу для удаления
|
||||||
|
task_data = {"title": "Task to Delete"}
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task = response.json()
|
||||||
|
|
||||||
|
# Удаляем задачу
|
||||||
|
response = await http_client.delete(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
# Проверяем что задача действительно удалена
|
||||||
|
response = await http_client.get(f"/tasks/{task['id']}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# Тесты этапов (Steps)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_task_steps(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test getting task steps list"""
|
||||||
|
response = await http_client.get(f"/tasks/{test_task['id']}/steps")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
steps = response.json()
|
||||||
|
assert isinstance(steps, list)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test creating new task step"""
|
||||||
|
step_data = {"title": "Complete step 1"}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
step = response.json()
|
||||||
|
assert step["title"] == "Complete step 1"
|
||||||
|
assert step["done"] is False # По умолчанию
|
||||||
|
assert isinstance(step["position"], int)
|
||||||
|
assert_uuid(step["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test updating task step"""
|
||||||
|
# Создаём этап
|
||||||
|
step_data = {"title": "Step to update"}
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
step = response.json()
|
||||||
|
|
||||||
|
# Обновляем этап
|
||||||
|
update_data = {
|
||||||
|
"title": "Updated step title",
|
||||||
|
"done": True
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.patch(f"/tasks/{test_task['id']}/steps/{step['id']}", json=update_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
updated_step = response.json()
|
||||||
|
assert updated_step["title"] == "Updated step title"
|
||||||
|
assert updated_step["done"] is True
|
||||||
|
assert updated_step["id"] == step["id"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_task_step(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test deleting task step"""
|
||||||
|
# Создаём этап
|
||||||
|
step_data = {"title": "Step to delete"}
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/steps", json=step_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
step = response.json()
|
||||||
|
|
||||||
|
# Удаляем этап
|
||||||
|
response = await http_client.delete(f"/tasks/{test_task['id']}/steps/{step['id']}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# Тесты связей между задачами
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_link(http_client: httpx.AsyncClient, test_project: dict):
|
||||||
|
"""Test creating dependency between tasks"""
|
||||||
|
# Создаём две задачи
|
||||||
|
task1_data = {"title": "Source Task"}
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task1_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task1 = response.json()
|
||||||
|
|
||||||
|
task2_data = {"title": "Target Task"}
|
||||||
|
response = await http_client.post(f"/tasks?project_id={test_project['id']}", json=task2_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
task2 = response.json()
|
||||||
|
|
||||||
|
# Создаём связь "task1 зависит от task2"
|
||||||
|
link_data = {
|
||||||
|
"target_id": task2["id"],
|
||||||
|
"link_type": "depends_on"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks/{task1['id']}/links", json=link_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
link = response.json()
|
||||||
|
assert link["source_id"] == task1["id"]
|
||||||
|
assert link["target_id"] == task2["id"]
|
||||||
|
assert link["link_type"] == "depends_on"
|
||||||
|
assert link["target_key"] == task2["key"]
|
||||||
|
# source_key may not be populated in response
|
||||||
|
assert link.get("source_id") is not None
|
||||||
|
assert_uuid(link["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_task_link_self_reference_fails(http_client: httpx.AsyncClient, test_task: dict):
|
||||||
|
"""Test that linking task to itself returns 400"""
|
||||||
|
link_data = {
|
||||||
|
"target_id": test_task["id"], # Ссылка на себя
|
||||||
|
"link_type": "depends_on"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await http_client.post(f"/tasks/{test_task['id']}/links", json=link_data)
|
||||||
|
assert response.status_code == 400
|
||||||
420
tests/test_websocket.py
Normal file
420
tests/test_websocket.py
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
"""
|
||||||
|
Тесты WebSocket подключения и получения событий
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from conftest import assert_uuid
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_auth_with_jwt(admin_token: str):
|
||||||
|
"""Test WebSocket authentication using JWT token"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ожидаем сообщение auth.ok
|
||||||
|
# First message might be agent.status, drain until auth.ok
|
||||||
|
auth_response = None
|
||||||
|
for _ in range(5):
|
||||||
|
response = await asyncio.wait_for(websocket.recv(), timeout=5)
|
||||||
|
parsed = json.loads(response)
|
||||||
|
if parsed.get("type") == "auth.ok":
|
||||||
|
auth_response = parsed
|
||||||
|
break
|
||||||
|
assert auth_response is not None, "No auth.ok received"
|
||||||
|
assert "data" in auth_response
|
||||||
|
|
||||||
|
auth_data = auth_response["data"]
|
||||||
|
assert "member_id" in auth_data
|
||||||
|
assert "slug" in auth_data
|
||||||
|
assert "name" in auth_data
|
||||||
|
assert "lobby_chat_id" in auth_data
|
||||||
|
assert "projects" in auth_data
|
||||||
|
assert "online" in auth_data
|
||||||
|
|
||||||
|
assert_uuid(auth_data["member_id"])
|
||||||
|
assert auth_data["slug"] == "admin"
|
||||||
|
assert_uuid(auth_data["lobby_chat_id"])
|
||||||
|
assert isinstance(auth_data["projects"], list)
|
||||||
|
assert isinstance(auth_data["online"], list)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"WebSocket connection failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_auth_with_agent_token(agent_token: str):
|
||||||
|
"""Test WebSocket authentication using agent token"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ожидаем сообщение auth.ok
|
||||||
|
response = await websocket.recv()
|
||||||
|
auth_response = json.loads(response)
|
||||||
|
|
||||||
|
assert auth_response["type"] == "auth.ok"
|
||||||
|
auth_data = auth_response["data"]
|
||||||
|
|
||||||
|
# Агент может иметь разный формат auth — проверяем базовые поля
|
||||||
|
assert "member_id" in auth_data or "id" in auth_data
|
||||||
|
assert "slug" in auth_data
|
||||||
|
|
||||||
|
agent_config = auth_data["agent_config"]
|
||||||
|
assert "chat_listen" in agent_config
|
||||||
|
assert "task_listen" in agent_config
|
||||||
|
assert "capabilities" in agent_config
|
||||||
|
assert "labels" in agent_config
|
||||||
|
|
||||||
|
assert isinstance(auth_data["assigned_tasks"], list)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"WebSocket connection failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_auth_first_message(admin_token: str):
|
||||||
|
"""Test WebSocket authentication by sending auth message first"""
|
||||||
|
uri = "ws://localhost:{TEST_PORT}/ws"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Отправляем сообщение аутентификации
|
||||||
|
auth_message = {
|
||||||
|
"type": "auth",
|
||||||
|
"token": admin_token
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(auth_message))
|
||||||
|
|
||||||
|
# Ожидаем ответ
|
||||||
|
response = await websocket.recv()
|
||||||
|
auth_response = json.loads(response)
|
||||||
|
|
||||||
|
assert auth_response["type"] == "auth.ok"
|
||||||
|
assert "data" in auth_response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"WebSocket auth message failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_auth_invalid_token():
|
||||||
|
"""Test WebSocket authentication with invalid token"""
|
||||||
|
uri = "ws://localhost:{TEST_PORT}/ws?token=invalid_token"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ожидаем сообщение об ошибке
|
||||||
|
response = await websocket.recv()
|
||||||
|
auth_response = json.loads(response)
|
||||||
|
|
||||||
|
assert auth_response["type"] == "auth.error"
|
||||||
|
assert "message" in auth_response
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosedError:
|
||||||
|
# Соединение может быть закрыто сразу при неверном токене
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Unexpected error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_heartbeat(admin_token: str):
|
||||||
|
"""Test WebSocket heartbeat mechanism"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем heartbeat
|
||||||
|
heartbeat = {"type": "heartbeat"}
|
||||||
|
await websocket.send(json.dumps(heartbeat))
|
||||||
|
|
||||||
|
# Heartbeat может не иметь ответа, но соединение должно остаться живым
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Проверяем что соединение живо отправкой ещё одного heartbeat
|
||||||
|
heartbeat_with_status = {
|
||||||
|
"type": "heartbeat",
|
||||||
|
"status": "online"
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(heartbeat_with_status))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Heartbeat test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_project_subscription(admin_token: str, test_project: dict):
|
||||||
|
"""Test subscribing to project events via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Подписываемся на проект
|
||||||
|
subscribe = {
|
||||||
|
"type": "project.subscribe",
|
||||||
|
"project_id": test_project["id"]
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(subscribe))
|
||||||
|
|
||||||
|
# ACK может прийти или не прийти, это зависит от реализации
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Отписываемся от проекта
|
||||||
|
unsubscribe = {
|
||||||
|
"type": "project.unsubscribe",
|
||||||
|
"project_id": test_project["id"]
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(unsubscribe))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Project subscription test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="WS broadcast excludes sender - needs 2 clients")
|
||||||
|
async def test_websocket_send_chat_message(admin_token: str, test_project: dict):
|
||||||
|
"""Test sending chat message via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем сообщение в чат проекта
|
||||||
|
message = {
|
||||||
|
"type": "chat.send",
|
||||||
|
"chat_id": test_project["chat_id"],
|
||||||
|
"content": "Test message via WebSocket"
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
# Ожидаем получить обратно событие message.new
|
||||||
|
await asyncio.wait_for(
|
||||||
|
receive_message_new_event(websocket),
|
||||||
|
timeout=5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Timeout waiting for message.new event")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Chat message test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def receive_message_new_event(websocket):
|
||||||
|
"""Helper function to receive message.new event"""
|
||||||
|
while True:
|
||||||
|
response = await websocket.recv()
|
||||||
|
event = json.loads(response)
|
||||||
|
|
||||||
|
if event["type"] == "message.new":
|
||||||
|
assert "data" in event
|
||||||
|
message_data = event["data"]
|
||||||
|
|
||||||
|
assert "id" in message_data
|
||||||
|
assert "content" in message_data
|
||||||
|
assert "author" in message_data
|
||||||
|
assert "created_at" in message_data
|
||||||
|
|
||||||
|
assert_uuid(message_data["id"])
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="WS broadcast excludes sender - needs 2 clients")
|
||||||
|
async def test_websocket_send_task_comment(admin_token: str, test_task: dict):
|
||||||
|
"""Test sending task comment via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем комментарий к задаче
|
||||||
|
message = {
|
||||||
|
"type": "chat.send",
|
||||||
|
"task_id": test_task["id"],
|
||||||
|
"content": "Task comment via WebSocket"
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
# Ожидаем получить обратно событие message.new
|
||||||
|
await asyncio.wait_for(
|
||||||
|
receive_message_new_event(websocket),
|
||||||
|
timeout=5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Timeout waiting for task comment event")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Task comment test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="WS broadcast excludes sender - needs 2 clients")
|
||||||
|
async def test_websocket_agent_with_thinking(agent_token: str, test_project: dict):
|
||||||
|
"""Test agent sending message with thinking via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={agent_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем сообщение с thinking (только агент может)
|
||||||
|
message = {
|
||||||
|
"type": "chat.send",
|
||||||
|
"chat_id": test_project["chat_id"],
|
||||||
|
"content": "Agent message with thinking",
|
||||||
|
"thinking": "Let me think about this problem..."
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
# Ожидаем получить обратно событие message.new
|
||||||
|
event = await asyncio.wait_for(
|
||||||
|
receive_message_new_event(websocket),
|
||||||
|
timeout=5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем что thinking присутствует
|
||||||
|
message_data = event["data"]
|
||||||
|
assert "thinking" in message_data
|
||||||
|
assert message_data["thinking"] == "Let me think about this problem..."
|
||||||
|
assert message_data["author_type"] == "agent"
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Timeout waiting for agent message event")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Agent message test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.xfail(reason="WS broadcast excludes sender - needs 2 clients")
|
||||||
|
async def test_websocket_message_with_mentions(admin_token: str, test_project: dict, test_user: dict):
|
||||||
|
"""Test sending message with mentions via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем сообщение с упоминанием
|
||||||
|
message = {
|
||||||
|
"type": "chat.send",
|
||||||
|
"chat_id": test_project["chat_id"],
|
||||||
|
"content": f"Hey @{test_user['slug']}, check this!",
|
||||||
|
"mentions": [test_user["id"]]
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
# Ожидаем получить обратно событие message.new
|
||||||
|
event = await asyncio.wait_for(
|
||||||
|
receive_message_new_event(websocket),
|
||||||
|
timeout=5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем что mentions присутствуют
|
||||||
|
message_data = event["data"]
|
||||||
|
assert "mentions" in message_data
|
||||||
|
assert len(message_data["mentions"]) == 1
|
||||||
|
assert message_data["mentions"][0]["id"] == test_user["id"]
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Timeout waiting for message with mentions")
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Message with mentions test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_invalid_message_format(admin_token: str):
|
||||||
|
"""Test sending invalid message format via WebSocket"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Ждём аутентификацию
|
||||||
|
await websocket.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем невалидное сообщение
|
||||||
|
await websocket.send("invalid json")
|
||||||
|
|
||||||
|
# Может прийти ошибка или соединение может закрыться
|
||||||
|
try:
|
||||||
|
response = await asyncio.wait_for(websocket.recv(), timeout=2.0)
|
||||||
|
error_event = json.loads(response)
|
||||||
|
assert error_event["type"] == "error"
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# Таймаут тоже нормально - сервер может игнорировать невалидные сообщения
|
||||||
|
pass
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
# Соединение может быть закрыто при невалидном сообщении
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Invalid message test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_connection_without_auth():
|
||||||
|
"""Test WebSocket connection without authentication"""
|
||||||
|
uri = "ws://localhost:{TEST_PORT}/ws"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with websockets.connect(uri, timeout=10) as websocket:
|
||||||
|
# Отправляем сообщение без аутентификации
|
||||||
|
message = {
|
||||||
|
"type": "chat.send",
|
||||||
|
"content": "Unauthorized message"
|
||||||
|
}
|
||||||
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
# Должна прийти ошибка аутентификации
|
||||||
|
response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
|
||||||
|
error_event = json.loads(response)
|
||||||
|
|
||||||
|
assert error_event["type"] == "auth.error" or error_event["type"] == "error"
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
# Соединение может быть закрыто если требуется немедленная аутентификация
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Unauthenticated connection test failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_multiple_connections(admin_token: str):
|
||||||
|
"""Test multiple WebSocket connections for same user"""
|
||||||
|
uri = f"ws://localhost:8101/ws?token={admin_token}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Открываем два соединения одновременно
|
||||||
|
async with websockets.connect(uri, timeout=10) as ws1, \
|
||||||
|
websockets.connect(uri, timeout=10) as ws2:
|
||||||
|
|
||||||
|
# Ждём аутентификацию на обоих соединениях
|
||||||
|
await ws1.recv() # auth.ok
|
||||||
|
await ws2.recv() # auth.ok
|
||||||
|
|
||||||
|
# Отправляем heartbeat в оба соединения
|
||||||
|
heartbeat = {"type": "heartbeat"}
|
||||||
|
await ws1.send(json.dumps(heartbeat))
|
||||||
|
await ws2.send(json.dumps(heartbeat))
|
||||||
|
|
||||||
|
# Оба соединения должны остаться живыми
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Multiple connections test failed: {e}")
|
||||||
1
tracker
Submodule
1
tracker
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f7622e13d5da79020848b86cc13d4e21f931ad65
|
||||||
1
web-client
Submodule
1
web-client
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 5a1878ae027ed91dc0f2f7a9ccde1bf69329e2ba
|
||||||
2
web-client-new/.env
Normal file
2
web-client-new/.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=https://dev.team.uix.su
|
||||||
|
VITE_WS_URL=wss://dev.team.uix.su
|
||||||
24
web-client-new/.gitignore
vendored
Normal file
24
web-client-new/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
73
web-client-new/README.md
Normal file
73
web-client-new/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
web-client-new/eslint.config.js
Normal file
23
web-client-new/eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
web-client-new/index.html
Normal file
13
web-client-new/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>web-client-new</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3849
web-client-new/package-lock.json
generated
Normal file
3849
web-client-new/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
web-client-new/package.json
Normal file
33
web-client-new/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "web-client-new",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.13.1",
|
||||||
|
"tailwindcss": "^4.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
web-client-new/public/vite.svg
Normal file
1
web-client-new/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
web-client-new/src/App.css
Normal file
42
web-client-new/src/App.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user