[2026-09-26] eagle: personal/plans/balda-mm-endtoend-plan.md
This commit is contained in:
1 parent
02f8656ed8
commit
bf7fa3569d
1 file changed
+180
@@ -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]] — редакция секретов
|
||||
Reference in new issue
Block a user