Files
obsidian-vault/personal/plans/balda-mm-endtoend-plan.md
T

11 KiB
Raw Blame History

План: доделать сквозной путь @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):

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 — Сборка и деплой ⬜

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) ⬜

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"

Связанные заметки