192 lines
8.8 KiB
Markdown
192 lines
8.8 KiB
Markdown
# 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:
|
||
|
||
1. **Извлечь @mention** — найти все `@**Name**` в тексте (Zulip-формат). Игнорировать `@**all**` и `@**everyone**`.
|
||
|
||
2. **Проверить источник @mention:**
|
||
- Если сообщение от **человека** (sender_id не из списка ботов) и содержит `@**<бот>**`:
|
||
- Записать этого бота как владельца треда (stream+topic)
|
||
- FORWARD сообщение этому боту
|
||
- Если сообщение от **человека** и содержит @mention **другого бота** (из конфига, но не того, что стал бы овнером):
|
||
- Всё равно записать этого бота как владельца треда
|
||
- НЕ форвардить (Zulip сам отправит webhook целевому боту по @mention)
|
||
- Если сообщение от **бота** (из конфига) и содержит @mention другого бота:
|
||
- Обновить ownership, но НЕ форвардить (Zulip сам доставит)
|
||
|
||
3. **Нет @mention, но тред уже закреплён за ботом:**
|
||
- FORWARD владельцу треда
|
||
|
||
4. **Новый тред без @mention:**
|
||
- FORWARD default-боту (из конфига)
|
||
|
||
5. **reset / system-команды без @mention:**
|
||
- FORWARD только владельцу треда
|
||
|
||
### Важный нюанс: @mention от другого бота
|
||
|
||
Когда один бот пишет `@Валера` (например, Eagle), Zulip отправляет outgoing webhook Валере напрямую — роутер не должен дублировать это сообщение. Поэтому:
|
||
|
||
- Роутер **ставит ownership**, но **не форвардит** если в сообщении есть @mention любого бота из его конфига
|
||
- Целевой бот получит сообщение через свой собственный outgoing webhook от Zulip
|
||
|
||
## Конфиг роутера (`~/Docker/zulip-router/config.yaml`)
|
||
|
||
```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):
|
||
|
||
```yaml
|
||
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`:
|
||
|
||
```env
|
||
# Credentials Eagle/Орла — из ~/.hermes/config.yaml
|
||
ZULIP_API_KEY=<скопировать из конфига Eagle>
|
||
```
|
||
|
||
### Запуск
|
||
|
||
```bash
|
||
cd ~/Docker/zulip-router
|
||
docker-compose up -d
|
||
docker logs zulip-router --tail 20 # проверить poll loop
|
||
```
|
||
|
||
## TODO
|
||
|
||
- [x] Написать код роутера (~260 LOC)
|
||
- [x] Создать compose `~/Docker/zulip-router/docker-compose.yaml`
|
||
- [x] Набить `.env` с credentials Eagle (из `~/.hermes/config.yaml`)
|
||
- [x] `docker-compose up -d`
|
||
- [x] Проверить логи: `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`), не соответствующая реальному бинарю в контейнере. Актуальный код роутера не найден.
|