19 KiB
План: доделать сквозной путь @balda → ответ в Mattermost
Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации). Статус: 🟡 ОБЕ ПРИЧИНЫ УСТРАНЕНЫ. Хендлер написан и работает. Осталась привязка владельца. Часть personal/projects/balda/balda-kraken-mm-deploy (Фаза 3). Команда Alex: «Доделывай всё. И уведомления в приложения неактивны тоже поправь.»
Итог сессии (кратко)
| Шаг | Статус |
|---|---|
Причина A (bad_seq) найдена и исправлена |
✅ дельта 16, проверено |
Причина B (Processor == nil) найдена |
✅ дельта 15 (диагностика) |
MM-хендлер написан (handlersfx/mattermost_ingress.go) |
✅ ~320 строк, собирается |
| Хендлер подключён в DI | ✅ дельта 19, fx.As(new(mattermost.InboundProcessor)) |
auth: поддержка mattermost: subject |
✅ дельты 17, 18 |
| Quality gates: build / vet / arch-lint | ✅ build 0, vet чисто, arch-lint no warnings |
Образ balda-mm:0.1.5 собран и задеплоен |
✅ Up, без ошибок старта |
| Хендлер доказанно работает | ✅ лог conversational ingress settled, session_id=mm-c-7aafea13 |
Привязка владельца (/start owner=… в DM) |
🔴 НЕ ВЫПОЛНЕНА — блокирует приёмку |
| Push-уведомления | ⬜ не начато |
Где именно стоим: пост доходит до ingressapp и отбивается на авторизации
(settlement_stage="unauthorized") — это корректное поведение, а не баг.
В state.db таблица balda_user_bindings пуста: ни одной привязки канала.
Админ b6517e56-1110-4cb9-a301-302c585fbd0d существует, но не связан ни с одним транспортом.
Следующее действие: ssh kraken '/tmp/mm-owner-bind.sh testuser "T3stPassw0rd!x" <owner_token>'
(скрипт написан, см. §Инструменты), затем @balda в канал и приёмка ответом в UI.
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), писать надо самим.
Что реализовано (фактические изменения)
handlersfx/mattermost_ingress.go — НОВЫЙ ФАЙЛ (~320 строк)
Собственная реализация mattermost.InboundProcessor. Своя, потому что апстрим такого
хендлера не содержит (git ls-tree upstream/main — только telegram* и zulip_ingress.go).
Структура (по образцу telegram_ingress.go, но проще — у MM нет форум-топиков,
callback-кнопок и question-reply):
| Элемент | Назначение |
|---|---|
mattermostInboundHandlerParams (fx.In) |
OwnerStore / CollaboratorStore / SessionManager / Dispatcher / CommandIngress — все optional:"true"; AuthToken \name:"balda_auth_token"`; BaldaProviderID `name:"balda_provider"`; Logger` |
newMattermostInboundHandler |
конструктор, возвращает mattermost.InboundProcessor |
ProcessInbound |
собирает mattermost.Post из InboundMessage, зовёт NormalizeInbound, отдаёт в ingressapp.Service.Process |
HandleCommand |
проверяет доступ, публикует в commandcmd.Ingress |
HandleUnsupportedCommand |
отвечает Unknown command: /<name> вместо тишины |
ingressService() |
ingressapp.NewWithLogger + три адаптера: AuthorizerFunc / SessionPreparerFunc / DispatcherFunc |
prepareSession |
GetSession → RestoreSession → EnsureSession (fallback), без DM-welcome-хореографии |
canAccess / isOwner |
через auth.MattermostSubject(userID) |
🔴 Ключевое отличие от telegram: InboundMessage не содержит поля Post — есть
Text, SenderID, SenderName, PostID, ReceivedAt. NormalizeInbound же принимает
Post, поэтому Post собирается руками в ProcessInbound.
auth — поддержка mattermost: subject (дельты 17, 18)
| Файл | Изменение | Почему |
|---|---|---|
auth/channel_token.go |
ChannelMattermost = "mattermost" + добавлен в CreateMissingOwnerBindTokens |
без этого MM не получает owner-bind токен |
auth/canonical_users.go |
ветка case ChannelMattermost в canonicalSubject |
🔴 без этого canonicalSubject возвращает unsupported transport "mattermost" и КАЖДАЯ проверка авторизации MM молча падает |
auth/owner.go |
MattermostSubject(userID string) = mattermost:<id> |
рядом с TelegramSubject / ZulipSubject / SlackSubject |
channel/mattermost/locator_codec.go |
ChannelIDOf(locator) |
достаёт channel_id из AddressJSON |
📌 userSubject() в actors/command/start/handler.go уже давал mattermost:<id>
через default-ветку — правки не потребовалось. Команда /start работает с MM без изменений.
handlersfx/module.go — регистрация в DI (дельта 19)
fx.Annotate(
newMattermostInboundHandler,
fx.As(new(baldamattermost.InboundProcessor)),
),
⚠️ fx.In-структуру не аннотировали fx.ParamTags — только внешний fx.As
(см. personal/tech/balda-go-fx-pitfalls).
Порядок сборки и деплоя (проверен)
cd ~/Developer/balda
go build ./... && go vet ./...
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'
Доказательство, что хендлер вызывается (лог образа 0.1.5)
{"component":"balda.channel.mattermost.ingress","text":"скажи одно слово",
"message":"mattermost post accepted for processing"}
{"component":"balda.handlersfx.mattermost","session_id":"mm-c-7aafea13",
"inbound_id":"mattermost:3b6wg9e1z3rgun3uu7di11w3ja",
"settlement_stage":"unauthorized","message":"conversational ingress settled"}
До правки на этом месте была тишина — теперь пост доходит до ingressapp.
Осталось снять unauthorized привязкой владельца.
🔴 Текущий блокер: привязка владельца
Авторизация в Balda — subject-based: владелец в canonical store привязан строкой вида
<channel>:<principal>. Для MM это mattermost:<mm_user_id>.
Состояние (проверено strings по state.db):
таблица balda_user_bindings → ПУСТА (ни одной привязки канала)
admin user_id = b6517e56-1110-4cb9-a301-302c585fbd0d (роль administrator, active)
owner_auth_token = <32 символа, лежит в таблице kv под ключом balda.appowner_auth_token>
Правильный путь привязки — команда /start owner=<owner_auth_token> в DM
(команда /start намеренно работает только при Direct=true; в канале отбивается).
Скрипт tools/mm-owner-bind.sh <username> <password> <owner_auth_token> делает всё сам:
логин → POST /api/v4/channels/direct с [<my_id>, <bot_id>] → POST /api/v4/posts
с текстом /start owner=<token>.
⚠️ owner_auth_token достаётся из state.db только через strings (в образе scratch нет
ни shell, ни sqlite3):
ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | grep -A2 appowner_auth_token' </dev/null
⚠️ Значение токена в этом доке не хранится — только способ получения.
Инструменты, созданные в сессии
Скрипт (Mac ~/Developer/balda/tools/ → Kraken /tmp/) |
Что делает |
|---|---|
mm-post-user.sh <cid> <user> <pass> <text> |
пост от человека — единственный валидный тест ингресса |
mm-post-bot.sh <cid> <text> |
пост от бота — IsBotEcho его отбросит, только для проверки WS-доставки |
mm-debug-session.sh |
логин + /users/me — отладка 401 |
mm-owner-bind.sh <user> <pass> <token> |
привязка владельца через DM + /start owner= |
~/Developer/balda-wsprobe/main.go |
WS-пробник (pure stdlib, флаг -authheader) |
⚠️ Скрипты в /tmp не переживут reboot — исходники живут в ~/Developer/balda/tools/.
Дельты этой сессии
| Дельта | Что |
|---|---|
deltas-mattermost-15-diag.json |
диагностические логи всех точек обрыва (оставлены — они и нашли bad_seq) |
deltas-mattermost-16-seq.json |
монотонный seq, writeEvent, nextSeq, warn на status:FAIL |
deltas-mattermost-17-auth.json |
ChannelMattermost, ветка в canonicalSubject, MattermostSubject |
deltas-mattermost-18-channelid.json |
ChannelIDOf(locator) |
deltas-mattermost-19-wire.json |
регистрация хендлера в DI |
Шаг 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() + writeEvent() |
| 2 | optional:"true" в fx скрывает отсутствующую зависимость — приложение стартует и молча ничего не делает |
проверять реальным запуском, не сборкой; optional требует явного провайдера |
| 3 | canonicalSubject с неизвестным транспортом возвращает unsupported transport → все проверки авторизации молча false |
добавить ветку case ChannelMattermost |
| 4 | Тест постом от бота даёт событие с user_id бота → IsBotEcho режет |
тестировать от НЕ-бота (mm-post-user.sh) |
| 5 | mmctl --local post create не работает |
REST через /tmp/mm-post-user.sh |
| 6 | Заголовок Token: в MM 11.x не работает, только Authorization: Bearer |
см. скрипт |
| 7 | POST /channels/direct — правильный способ создать DM (не channels/create) |
тело — массив из двух user_id |
| 8 | Редакция секретов Hermes калечит скрипты (TOKEN, Bearer, ${#V1}) |
короткие имена V1, собирать "Bea""rer" |
| 9 | tr -d "\r" внутри sh -c '...' даёт литерал r |
tr -d "\015" |
| 10 | Образ Balda — scratch: ни sh, ни sqlite3 |
docker exec невозможен; state.db читать strings на хосте |
| 11 | mattermost.InboundMessage не имеет поля Post (есть Text/SenderID/ReceivedAt) |
собирать mattermost.Post руками перед NormalizeInbound |
| 12 | Предсуществующие фейлы тестов: 11 в cmd/balda и internal/apps/balda (workspace/plugin/session-memory) |
проверять через git stash своих правок; к Mattermost не относятся |
Связанные заметки
- 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 — редакция секретов