From bf7fa3569d1046912e7b58865fed70f33ade021d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sat, 26 Sep 2026 13:26:09 +0600 Subject: [PATCH] [2026-09-26] eagle: personal/plans/balda-mm-endtoend-plan.md --- personal/plans/balda-mm-endtoend-plan.md | 180 +++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 personal/plans/balda-mm-endtoend-plan.md diff --git a/personal/plans/balda-mm-endtoend-plan.md b/personal/plans/balda-mm-endtoend-plan.md new file mode 100644 index 00000000..7e7c0e78 --- /dev/null +++ b/personal/plans/balda-mm-endtoend-plan.md @@ -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]] — редакция секретов