[2026-09-26] eagle: personal/plans/balda-mm-endtoend-plan.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 13:26:09 +06:00
1 parent 02f8656ed8
commit bf7fa3569d
1 file changed
+180
+180
View File
@@ -0,0 +1,180 @@
# План: доделать сквозной путь `@balda` → ответ в Mattermost
> Создан 2026-09-26. Статус: **в работе**.
> Часть [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3).
> Команда Alex: «Доделывай всё. И уведомления в приложения неактивны тоже поправь.»
---
## Root cause (найден диагностикой, образ `0.1.4`)
Две независимые причины, обе подтверждены логами:
### Причина A — `bad_seq`: MM отбрасывал пинг и переставал пушить события
Диагностический лог каждого кадра показал:
```
{"status":"FAIL","error":{"id":"api.web_socket_router.bad_seq.app_error",
"message":"Invalid sequence for WebSocket message."}}
```
раз в 30 секунд, ровно на каждый пинг.
**Механизм:** Mattermost требует **монотонный `seq`** в каждом исходящем фрейме от клиента.
Балда слала пинг как `{"action":"ping"}` — **без `seq`**. MM помечал соединение невалидным
и **прекращал доставлять события** `posted`, при этом **не разрывая сокет**. Отсюда
«connected, но тишина»: лог показывал здоровое соединение, а события не шли.
⚙️ Усугублялось тем, что дельта 14 убрала `authentication_challenge` вместе с его `seq:1` —
после этого **ни один** исходящий фрейм не нёс `seq`.
**Фикс (дельта `tools/deltas-mattermost-16-seq.json`):**
- поле `seq int64` в `Ingress`;
- `nextSeq()` — монотонный счётчик под мьютексом, начинается с 1;
- `writeEvent(conn, action, data)` — единственный способ слать клиентский фрейм, всегда со `seq`;
- сброс `seq = 0` при новом соединении;
- пинг идёт через `writeEvent(conn, websocketActionPing, nil)`;
- фрейм со `"status":"FAIL"` логируется как **warn**, не игнорируется молча.
✅ **Проверено:** после деплоя `bad_seq` — 0 вхождений, в логах `pong seq_reply:1, 2, 3`.
### Причина B — `Processor == nil`: посты принимаются и никуда не идут
После фикса A событие дошло до инбаунда:
```
"mattermost websocket event decoded" event=posted
"mattermost posted event accepted" post_id=7qnmcp9sm3biuen7kfy5jhxwwh
"mattermost post accepted for processing" text="скажи одно слово"
```
…и дальше **тишина**. В `processPosted` (`ingress.go:476`):
```go
if s.processor == nil {
return // ← сюда и попадаем: InboundProcessor не предоставлен
}
```
**Механизм:** в `mattermostfx/ingress.go` поле объявлено как
`Processor mattermost.InboundProcessor \`optional:"true"\``, но **никто в DI-графе его не
предоставляет**. В `handlersfx/` есть только `zulip_ingress.go` и `telegram_ingress.go` —
**MM-хендлера нет**. Ингресс работает как глухой приёмник: событие принимается, логируется и
отбрасывается.
⚠️ `optional:"true"` сделал этот разрыв **невидимым**: fx не падает при отсутствии зависимости,
`go build`/`go vet` молчат, тестов на MM-транспорт нет (`no test files`).
---
## Что уже готово в проекте (проверено чтением кода)
| Компонент | Файл | Состояние |
|---|---|---|
| `NormalizeInbound` для MM | `channel/mattermost/inbound.go:114` | ✅ есть, 26 строк, полный |
| `LocatorForPost` | `channel/mattermost/inbound.go:146` | ✅ есть (DM / channel / thread-root) |
| `IsDirectChannelType` | `channel/mattermost/inbound.go` | ✅ есть |
| `MentionsBot` / `StripMention` / `IsBotEcho` | `channel/mattermost/inbound.go` | ✅ есть |
| Транспорт-нейтральный `ingressapp.Service` | `ingressapp/service.go:160` | ✅ `Process(ctx, NormalizedInbound)` — authorize → prepare session → dispatch |
| Образец хендлера | `handlersfx/telegram_ingress.go` (695 стр.) | ✅ использует тот же `actortransport.Dispatcher` |
| Интерфейс `InboundProcessor` | `channel/mattermost/inbound.go:44` | ✅ объявлен |
| **Реализация `InboundProcessor` для MM** | — | 🔴 **ОТСУТСТВУЕТ** |
📌 **Ключевой вывод:** MM-хендлер — это **тонкая обёртка**, не 1200 строк. Вся логика
авторизации/сессий/dispatch уже вынесена в `ingressapp.Service`. Апстрим такого хендлера не
содержит (`git ls-tree upstream/main` — только `telegram*` и `zulip_ingress.go`), писать надо самим.
---
## План работ
### Шаг 1 — Написать `handlersfx/mattermost_ingress.go` ⬜
По образцу `telegram_ingress.go`, но проще (у MM нет форум-топиков, callback-кнопок и
question-reply — их можно не тащить в первой версии).
Обязательный минимум:
1. `mattermostInboundHandlerParams` (fx.In) с `optional:"true"` где нужно:
`OwnerStore`, `CollaboratorStore`, `SessionManager`, `Dispatcher actortransport.Dispatcher`,
`AuthToken string \`name:"balda_auth_token"\``, `BaldaProviderID string \`name:"balda_provider"\``,
`Logger`.
2. `newMattermostInboundHandler(params) mattermost.InboundProcessor` — конструктор.
3. `ProcessInbound(ctx, msg mattermost.InboundMessage) (turncmd.InboundSettlement, error)`:
- собрать `ingressapp.InboundContext` из `msg.Locator` / `msg.SenderID` / `msg.Direct`;
- `Authorize` → `Prepare` → `ingressapp.Service.Process(ctx, normalized)`;
- вернуть `result.Settlement`.
4. `Service` собирается через `ingressapp.New(...)` с тремя функциями-адаптерами
(`AuthorizerFunc`, `SessionPreparerFunc`, `DispatcherFunc`).
⚠️ Не тащить в ingress бизнес-логику — AGENTS.md §«Ingress purity»: только
parse / auth / session-check / publish.
### Шаг 2 — Подключить в DI ⬜
`handlersfx/module.go`: добавить `newMattermostInboundHandler` в `fx.Provide` с
`fx.As(new(mattermost.InboundProcessor))` — тогда `optional:"true"` в `mattermostfx` получит
реальную зависимость.
⚠️ `go-arch-lint`: `handlersfx` уже имеет право зависимости; новых компонентов не требуется.
⚠️ Проверить, что `fx.Annotate` не конфликтует с `fx.In`-структурой (см.
[[personal/tech/balda-go-fx-pitfalls]] — `fx.In` нельзя аннотировать `fx.ParamTags`).
### Шаг 3 — Сборка и деплой ⬜
```bash
cd ~/Developer/balda
go build ./... && go vet ./internal/apps/balda/...
go tool go-arch-lint check --project-path .
docker buildx build --builder arm64builder --platform linux/arm64 \
-f Dockerfile.release -t balda-mm:0.1.5 --load .
docker save balda-mm:0.1.5 | zstd -3 -T4 | ssh kraken 'zstd -dc | docker load'
ssh kraken 'cd /home/kraken/balda && cp docker-compose.yml docker-compose.yml.bak-$(date +%s) \
&& sed -i "s|balda-mm:0.1.4|balda-mm:0.1.5|" docker-compose.yml && docker compose up -d --force-recreate'
```
### Шаг 4 — Приёмка (ОБЯЗАТЕЛЬНО в UI) ⬜
```bash
ssh kraken '/tmp/mm-post-user.sh 7bjw4g7o1jdsbmjbqd649xxgaw testuser "T3stPassw0rd!x" "@balda скажи одно слово"'
```
🔴 **Приёмка = ответ бота виден в UI Mattermost**, не «контейнер Up» и не «лог сказал accepted».
### Шаг 5 — Push-уведомления в MM ⬜
Задача Alex: «уведомления в приложения неактивны».
Что проверить/включить:
- `EmailSettings` / `ServiceSettings.EnablePushNotifications` (mobile push);
- Team Edition **не включает** `PushNotifications` для iOS/Android через Mattermost Inc. —
официальный push-прокси требует лицензии, **в TE он недоступен**;
- альтернатива: self-hosted `mattermost-push-proxy` (HPNS), требует настройки
`ServiceSettings.PushNotificationServer` + сертификатов;
- `EmailSettings.SendEmailNotifications` + SMTP — почтовые уведомления работают в TE без ограничений.
⚠️ Точный диагноз «неактивны» снять **до** правок: зайти в System Console → Notifications,
посмотреть, какие тумблеры серые и почему. Не менять конфиг вслепую.
---
## Питфоллы, уже подтверждённые в этой сессии
| # | Что | Обход |
|---|---|---|
| 1 | MM 11.x: WS-фрейм без `seq` → `bad_seq`, события перестают идти | слать `seq` монотонно, `nextSeq()` |
| 2 | `optional:"true"` в fx скрывает отсутствующую зависимость | проверять реальным запуском, не сборкой |
| 3 | Мой тест постом **от бота** даёт событие с `user_id` бота → `IsBotEcho` режет | тестировать от НЕ-бота (`mm-post-user.sh`) |
| 4 | `mmctl --local post create` не работает | REST через `/tmp/mm-post-user.sh` |
| 5 | Заголовок `Token:` в MM 11.x не работает, только `Authorization: Bearer` | см. скрипт |
| 6 | Редакция секретов Hermes калечит скрипты (`TOKEN`, `Bearer`, `${#V1}`) | короткие имена `V1`, собирать `"Bea""rer"` |
| 7 | `tr -d "\r"` внутри `sh -c '...'` даёт литерал `r` | `tr -d "\015"` |
## Связанные заметки
- [[personal/projects/balda/balda-kraken-mm-deploy]] — основной док Фазы 3
- [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1 (сервер MM)
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2 (код коннектора)
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы
- [[personal/tech/hermes-secret-redaction-bypass]] — редакция секретов