12 KiB
Team Board — Telegram Bridge
Назначение
Telegram Bridge — микросервис для интеграции Team Board с Telegram. Обеспечивает двустороннюю синхронизацию сообщений между проектными чатами Tracker и топиками Telegram группы.
Основной файл: bridge.py
Архитектура: Standalone Python сервис
Архитектура
Telegram Bot API ↔ Bridge ↔ Tracker WebSocket
↓
Topic Mapping
(JSON файл хранение)
Компоненты:
- bridge.py — основной сервис с двусторонней синхронизацией
- config.py — конфигурация через environment переменные
- topic_map.py — персистентное маппинг Telegram топик ↔ Tracker проект
- tracker_client.py — WebSocket клиент для подключения к Tracker
Конфигурация
Файл: .env
# Telegram Bot
TELEGRAM_BOT_TOKEN=1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ
TELEGRAM_GROUP_ID=-1001234567890
# Tracker
TRACKER_URL=https://dev.team.uix.su
TRACKER_WS_URL=wss://dev.team.uix.su/ws
BRIDGE_TOKEN=tb-bridge-dev-token
Переменные окружения:
- TELEGRAM_BOT_TOKEN — токен бота от @BotFather
- TELEGRAM_GROUP_ID — ID группы с топиками (отрицательное число)
- TRACKER_URL — HTTP URL Tracker API
- TRACKER_WS_URL — WebSocket URL для подключения к Tracker
- BRIDGE_TOKEN — Bearer токен агента типа "bridge" в Tracker
Маппинг топиков
Файл: data/topic_map.json
Структура:
{
"123": "550e8400-e29b-41d4-a716-446655440000",
"456": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
Формат: {telegram_topic_id: tracker_project_uuid}
Класс TopicMap:
class TopicMap:
def set(topic_id: int, project_uuid: str) # создать маппинг
def get_project(topic_id: int) -> str | None # получить проект по топику
def get_topic(project_uuid: str) -> int | None # получить топик по проекту
def remove_by_topic(topic_id: int) # удалить маппинг
def all() -> dict[int, str] # все маппинги
Персистентность: автоматическое сохранение в JSON файл при изменениях
Telegram → Tracker
Обработка сообщений:
async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
# Фильтрация по группе и наличию topic
if message.chat_id != config.group_id or not message.message_thread_id:
return
# Поиск соответствующего проекта
project_uuid = topic_map.get_project(message.message_thread_id)
if not project_uuid:
return
# Формат сообщения: [Имя пользователя] текст
text = f"[{user.full_name}] {message.text}"
await tracker.send_message(project_uuid, text)
Telegram команды:
/link <project_slug>
Привязать текущий топик к проекту
Использование:
/link team-board
Логика:
- Проверка, что команда выполнена в топике
- Поиск проекта по slug через Tracker API
- Создание маппинга в TopicMap
- Подтверждение в чате
/unlink
Отвязать текущий топик от проекта
Логика:
- Проверка выполнения в топике
- Удаление маппинга из TopicMap
- Подтверждение в чате
/bridge_status
Показать статус bridge и все активные маппинги
Пример ответа:
Привязки топик → проект:
• topic 123 → 550e8400...
• topic 456 → 6ba7b810...
Tracker → Telegram
Поддерживаемые события:
message.new
Новое сообщение в проектном чате
Фильтрация:
- Пропуск сообщений от самого bridge (избежание эха)
- Пропуск сообщений с префиксом
[Username](отправленных bridge'ом)
Формат в Telegram:
[Автор]: текст сообщения
Silent режим: если есть онлайн участники-люди в web интерфейсе
task.created
Создание новой задачи
Формат в Telegram:
📋 Новая задача XX-123: Название задачи
project.created
Создание нового проекта
Автоматическое действие:
- Создание топика в Telegram с именем проекта
- Автоматический маппинг топик → проект
- Уведомление о привязке
agent.status
Изменение статуса участника
Отслеживание: обновление множества онлайн участников для silent режима
TrackerClient
Файл: tracker_client.py
WebSocket подключение:
class TrackerClient:
async def connect() # подключение с авто-переподключением
async def send_message(project_uuid, text) # отправка сообщения в проект
def stop() # остановка клиента
Особенности:
- Auto-reconnect: переподключение каждые 5 секунд при разрыве
- Heartbeat: отправка heartbeat каждые 25 секунд
- Queue система: очередь сообщений до успешного подключения
- Project caching: кэширование project_uuid → chat_id маппингов
Аутентификация:
url = f"{config.tracker_ws_url}?token={config.bridge_token}"
Тип участника: bridge в Tracker системе
Работа с проектами
Кэширование chat_id:
self._project_chat_ids: dict[str, str] = {} # project_uuid -> chat_id
Загрузка при подключении:
GET /api/v1/projects
Authorization: Bearer {bridge_token}
Lazy loading: автоматическая подгрузка chat_id при необходимости
Отправка сообщений:
- Получение chat_id для проекта
- Формирование WebSocket сообщения:
{
"type": "chat.send",
"chat_id": "project_chat_uuid",
"content": "[Username] message text"
}
Silent режим
Логика: если в системе есть онлайн участники-люди (не агенты/bridge), уведомления в Telegram отправляются беззвучно
Отслеживание онлайн:
_online_members: set[str] = set() # member slugs
# При получении agent.status события:
if status == "online":
_online_members.add(slug)
else:
_online_members.discard(slug)
# При отправке в Telegram:
silent = any(s for s in _online_members if s not in ("coder", "architect", "bridge"))
Исключения: агенты coder, architect, bridge не считаются "людьми"
HTML Escaping
Все сообщения в Telegram экранируются для HTML:
def _escape_html(text: str) -> str:
return text.replace("&", "&").replace("<", "<").replace(">", ">")
Формат отправки: parse_mode="HTML"
Предотвращение эха
Проблема:
Bridge отправляет сообщение в Tracker → Tracker рассылает message.new event → Bridge получает и отправляет в Telegram → возможное дублирование
Решения:
1. Пропуск сообщений от bridge:
author_slug = author.get("slug", "")
if author_slug == "bridge":
return
2. Пропуск сообщений с Telegram префиксом:
if text_content.startswith("[") and "] " in text_content[:50]:
return
3. Исключение собственной сессии в WebSocket:
Tracker автоматически не отправляет события отправителю
Логирование
Уровень: INFO
Формат: %(asctime)s [%(name)s] %(levelname)s: %(message)s
Ключевые логи:
Connected to Tracker WS
Authenticated as bridge
TG->Tracker: topic=123 project=550e8400
Auto-created topic 456 for project team-board (6ba7b810)
Linked topic 123 -> project team-board (550e8400)
Запуск и развёртывание
Зависимости:
python-telegram-bot[webhooks]
websockets
httpx
python-dotenv
Файл: requirements.txt
Запуск:
python bridge.py
Архитектура событий:
async def main():
# Запуск Telegram polling
await app.updater.start_polling()
# Запуск Tracker WebSocket (блокирует)
await tracker.connect()
Concurrent execution: Telegram bot и Tracker WebSocket клиент работают параллельно
Обработка ошибок
Telegram API:
try:
await _bot.send_message(...)
except Exception as e:
logger.error("Failed to send to topic %d: %s", topic_id, e)
Tracker WebSocket:
try:
# WebSocket операции
except websockets.ConnectionClosed:
logger.warning("Tracker WS closed, reconnecting in 5s...")
await asyncio.sleep(5)
API запросы:
try:
async with httpx.AsyncClient() as client:
resp = await client.get(...)
if resp.status_code != 200:
return None
except Exception as e:
logger.error("API error: %s", e)
Стратегия: логирование ошибок + graceful degradation без остановки сервиса
Масштабирование
Ограничения:
- Один bridge instance на Telegram группу
- JSON файл для маппингов (не подходит для кластера)
- In-memory кэш проектов
Потенциальные улучшения:
- Redis для маппингов и кэша
- Database-backed конфигурация
- Multiple bot support
- Rate limiting для Telegram API
Безопасность
Аутентификация:
- Bridge токен для Tracker API/WebSocket
- Фильтрация по group_id в Telegram
- Только авторизованные команды
Данные:
- Маппинги хранятся локально в JSON
- Логирование без чувствительной информации
- Автоматическое переподключение при разрыве
Этот документ описывает Telegram Bridge на основе исходного кода в /root/projects/team-board/bridge/ на дату создания спецификации.