Files
obsidian-vault/personal/projects/zulip-router.md
T

192 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`), не соответствующая реальному бинарю в контейнере. Актуальный код роутера не найден.