docs/specs/BRIDGE.md

12 KiB
Raw Blame History

Team Board — Telegram Bridge

Назначение

Telegram Bridge — микросервис для интеграции Team Board с Telegram. Обеспечивает двустороннюю синхронизацию сообщений между проектными чатами Tracker и топиками Telegram группы.

Основной файл: bridge.py
Архитектура: Standalone Python сервис


Архитектура

Telegram Bot API ↔ Bridge ↔ Tracker WebSocket
                     ↓
               Topic Mapping
              (JSON файл хранение)

Компоненты:

  1. bridge.py — основной сервис с двусторонней синхронизацией
  2. config.py — конфигурация через environment переменные
  3. topic_map.py — персистентное маппинг Telegram топик ↔ Tracker проект
  4. 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 team-board

Логика:

  1. Проверка, что команда выполнена в топике
  2. Поиск проекта по slug через Tracker API
  3. Создание маппинга в TopicMap
  4. Подтверждение в чате

Отвязать текущий топик от проекта

Логика:

  1. Проверка выполнения в топике
  2. Удаление маппинга из TopicMap
  3. Подтверждение в чате

/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

Создание нового проекта

Автоматическое действие:

  1. Создание топика в Telegram с именем проекта
  2. Автоматический маппинг топик → проект
  3. Уведомление о привязке

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 при необходимости

Отправка сообщений:

  1. Получение chat_id для проекта
  2. Формирование 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

Формат отправки: 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/ на дату создания спецификации.