181 lines
11 KiB
Markdown
181 lines
11 KiB
Markdown
# План: доделать сквозной путь `@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]] — редакция секретов
|