8.8 KiB
Zulip Router
Последнее обновление: 2026-06-16 (обновлено Китом)
Цель
Event-роутер между Zulip и balda-ботами (Валера, Клавдий). Решает проблему конкурентного получения событий — сейчас оба бота видят все сообщения одновременно.
Проблема (root cause)
- Каждый balda-бот регистрирует собственную event queue в Zulip Events API
- Без роутера оба получают все сообщения, и каждый может ответить на @mention другого
extractOtherMention()патч в коде — костыль, не решает проблему полностью
Как сейчас (до роутера)
Zulip
├── Events API ← Валера (valera-bot, poll на :8091) — отключён
├── Events API ← Клавдий (claudio-bot, poll на :8092) — отключён
└── Webhook → оба (outgoing webhook через @mention)
Архитектура (с роутером)
Zulip Event Queue
↓ poll (credentials Eagle из ~/.hermes/config.yaml)
[zulip-router]
│
├── @mention бота от человека → POST owner-боту + ownership
├── @mention любого бота (включая другого) → ownership, НЕ форвардить
├── трейд с ownership → POST owner-боту
├── новый тред без @mention → default-боту (из конфига)
└── reset / system → только владельцу треда
Ключевое: credentials — Eagle/Орла (не новый бот). Роутер регистрирует свою event queue (отдельную от очереди Eagle).
Правила маршрутизации (пошагово)
Для каждого сообщения из event queue:
-
Извлечь @mention — найти все
@**Name**в тексте (Zulip-формат). Игнорировать@**all**и@**everyone**. -
Проверить источник @mention:
- Если сообщение от человека (sender_id не из списка ботов) и содержит
@**<бот>**:- Записать этого бота как владельца треда (stream+topic)
- FORWARD сообщение этому боту
- Если сообщение от человека и содержит @mention другого бота (из конфига, но не того, что стал бы овнером):
- Всё равно записать этого бота как владельца треда
- НЕ форвардить (Zulip сам отправит webhook целевому боту по @mention)
- Если сообщение от бота (из конфига) и содержит @mention другого бота:
- Обновить ownership, но НЕ форвардить (Zulip сам доставит)
- Если сообщение от человека (sender_id не из списка ботов) и содержит
-
Нет @mention, но тред уже закреплён за ботом:
- FORWARD владельцу треда
-
Новый тред без @mention:
- FORWARD default-боту (из конфига)
-
reset / system-команды без @mention:
- FORWARD только владельцу треда
Важный нюанс: @mention от другого бота
Когда один бот пишет @Валера (например, Eagle), Zulip отправляет outgoing webhook Валере напрямую — роутер не должен дублировать это сообщение. Поэтому:
- Роутер ставит ownership, но не форвардит если в сообщении есть @mention любого бота из его конфига
- Целевой бот получит сообщение через свой собственный outgoing webhook от Zulip
Конфиг роутера (~/Docker/zulip-router/config.yaml)
zulip:
bot_email: "eagle-bot@zulip.qentra.top"
api_key: "${ZULIP_API_KEY}"
server_url: "https://zulip.qentra.top"
bots:
- name: "Валера"
aliases: ["Valera"]
webhook: "http://balda-agent-valera-1:8091/zulip/webhook"
- name: "Клавдий"
aliases: ["Klavdiy"]
webhook: "http://claudio-agent-claudio-1:8092/zulip/webhook"
- name: "Eagle"
aliases: ["eagle", "Eagle"]
webhook: "http://host.docker.internal:8644/webhooks/eagle"
- name: "Кит"
aliases: ["Whale", "whale", "кит"]
webhook: "http://host.docker.internal:8644/webhooks/whale"
aliases: ["Klavdiy"]
webhook: "http://claudio-agent-claudio-1:8092/zulip/webhook"
# bot_ids — sender_id ботов в Zulip (чтобы отличать сообщения человека от бота)
bot_ids:
- 9 # Eagle / Орёл (Hermes)
- 10 # Клавдий (claudio-bot)
- 11 # Валера (balda-bot)
- 13 # Кит (whale-bot)
default_bot: "Валера"
ownership:
ttl: 24h
persist_path: "/data/ownership.json"
Что меняется в balda
После деплоя роутера Events API polling уже отключён у обоих ботов (сделано 2026-06-16):
zulip:
events_polling:
enabled: false
Сообщения приходят только через webhook от роутера (плюс outgoing webhook от Zulip при @mention напрямую).
Код
Репозиторий: ~/Developer/zulip-router/ — Go, ~260 LOC (план)
Git: git init 2026-06-16. Первый коммит: a403562 — чистый оригинал от 14:22. .bak файлы — слепки конфигов до правок Кита.
Deploy: ~/Docker/zulip-router/docker-compose.yaml
| Файл | Назначение |
|---|---|
main.go |
Poll loop, routing, dispatch |
config.go |
Config struct + YAML loading + env var expansion |
zulip.go |
Zulip API client (register_queue, get_events, long-poll) |
ownership.go |
Topic ownership store (RW mutex + JSON persistence) |
forwarder.go |
HTTP POST к balda webhook endpoints |
Deployment
Compose: ~/Docker/zulip-router/docker-compose.yaml
Сеть: balda_default (external) — чтобы видеть balda-agent-valera-1 и claudio-agent-claudio-1 по Docker DNS.
Credentials: Eagle, не новый бот
Роутер использует credentials Eagle/Орла из ~/.hermes/config.yaml → platforms.zulip.
Не создавать нового бота. Eagle уже имеет права на чтение всех публичных стримов.
Env vars
Создать ~/Docker/zulip-router/.env:
# Credentials Eagle/Орла — из ~/.hermes/config.yaml
ZULIP_API_KEY=<скопировать из конфига Eagle>
Запуск
cd ~/Docker/zulip-router
docker-compose up -d
docker logs zulip-router --tail 20 # проверить poll loop
TODO
- Написать код роутера (~260 LOC)
- Создать compose
~/Docker/zulip-router/docker-compose.yaml - Набить
.envс credentials Eagle (из~/.hermes/config.yaml) docker-compose up -d- Проверить логи:
docker logs zulip-router --tail 50 - Протестировать: @Клавдий и @Валера в разных топиках (человеком) — ПРОВАЛЕНО: роутер шлёт
trigger=ownerкогда @mention другого бота - Проверить ownership: сообщение в топик без @mention → последний бот
- Проверить @mention бота от другого бота → ownership ставится, дубляж нет — ПРОВАЛЕНО: см. #Проблема
- Удалить
extractOtherMention()патч из кода (больше не нужен)
Проблема
Роутер форвардит ВСЕ сообщения из треда, где есть ownership — даже если @mention адресован другому боту.
Логи:
forward → Валера (msg=XXXXX trigger=owner stream="personal" topic="Балда/валера")
По спецификации (см. правила маршрутизации п.2): если @mention другого бота — ownership ставится, но НЕ форвардить. Реальная имплементация этого не делает.
Причина: main.go в репозитории — старая версия (events poller с instances/targets), не соответствующая реальному бинарю в контейнере. Актуальный код роутера не найден.