[2026-09-26] eagle: family/how-to/jellyfin-config.md personal/plans/balda-mm-endtoend-plan.md personal/projects/balda/balda-kraken-mm-deploy.md personal/tech/balda-go-fx-pitfalls.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 13:31:13 +06:00
1 parent bf7fa3569d
commit 6ae7f397ef
5 files changed
+688 -86

No files matched your search

+194 -39
View File
@@ -1,13 +1,174 @@
# Фаза 3 — Балда на Kraken + Mattermost (деплой)
> Создан 2026-09-26. Обновлён 2026-09-26 (ночь, root-cause сессия).
> Статус: **🟡 РАЗВЁРНУТА. Найдены 3 причины тишины, 2 исправлены. Бот пока не отвечает.**
> Создан 2026-09-26. Обновлён 2026-09-26 (сессия реализации: `bad_seq` + MM-хендлер).
> Статус: **🟡 ОБЕ ПРИЧИНЫ ТИШИНЫ УСТРАНЕНЫ. Хендлер написан и доказанно вызывается.
> Блокирует одно: привязка владельца (`/start owner=` в DM).**
> Подробный план и фактические изменения — [[personal/plans/balda-mm-endtoend-plan]].
> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]].
> Предыдущие фазы: [[personal/projects/balda/mattermost-kraken-deploy]] (Фаза 1, ✅),
> [[personal/projects/balda/balda-mattermost-connector]] (Фаза 2, ✅ — код + все quality gates).
---
## 🔴 ГЛАВНОЕ: третья причина тишины найдена — это был НЕ ингресс, а MM-авторизация сокета
Прошлая сессия заканчивалась версией «событие теряется между `readLoop` и `handleFrame`».
**Версия оказалась неверной.** Диагностический лог каждого кадра (дельта 15) показал иную
картину: события **не приходили вообще**, а MM отвечал на пинги ошибкой.
### Причина 3 (настоящая) — `bad_seq`: MM отбрасывал пинг и переставал пушить события
```
{"status":"FAIL","error":{"id":"api.web_socket_router.bad_seq.app_error",
"message":"Invalid sequence for WebSocket message."}}
```
раз в 30 секунд. Балда слала пинг как `{"action":"ping"}` — **без `seq`**. Mattermost
требует монотонный `seq` в каждом клиентском фрейме; на невалидном он помечал соединение
сломанным и **переставал доставлять `posted`, не разрывая сокет**. Отсюда обманчивый
«`websocket connected`, но тишина».
⚙️ Усугублено дельтой 14: убрав `authentication_challenge`, я убрал и его `seq:1` — после
этого **ни один** исходящий фрейм не нёс `seq`.
**Фикс** — дельта `tools/deltas-mattermost-16-seq.json`: поле `seq int64` в `Ingress`,
`nextSeq()` под мьютексом, `writeEvent(conn, action, data)` как единственный путь отправки,
сброс счётчика на новом соединении, `status:FAIL` логируется warn'ом.
✅ Проверено: `bad_seq` — 0 вхождений, в логах `pong seq_reply:1, 2, 3`.
### Причина 4 — `Processor == nil`: посты принимались и молча отбрасывались
После фикса `bad_seq` событие дошло до инбаунда, залогировалось как
`mattermost post accepted for processing` — и дальше тишина. В `processPosted`:
```go
if s.processor == nil {
return // ← сюда и попадали
}
```
В `mattermostfx/ingress.go` поле объявлено `Processor mattermost.InboundProcessor \`optional:"true"\``,
но **никто в DI его не предоставлял**: в `handlersfx/` были только `zulip_ingress.go` и
`telegram_ingress.go`. **Ингресс работал глухим приёмником.**
⚠️ `optional:"true"` сделал разрыв невидимым: fx не падает, `go build`/`go vet` молчат,
тестов на MM-транспорт нет.
---
## Что сделано в этой сессии
### 1. Написан `handlersfx/mattermost_ingress.go` (~320 строк, новый файл)
Своя реализация `mattermost.InboundProcessor` — апстрим такого не содержит
(`git ls-tree upstream/main` даёт только `telegram*` и `zulip_ingress.go`).
Ключевое: **вся логика уже была вынесена в `ingressapp.Service`** (транспорт-нейтральный:
authorize → prepare session → dispatch), поэтому хендлер — **тонкая обёртка**, а не копия
1203-строчного zulip-хендлера. Образец — `telegram_ingress.go` (695 строк).
Методы: `ProcessInbound`, `HandleCommand` (публикует в `commandcmd.Ingress`),
`HandleUnsupportedCommand`, плюс `ingressService()` / `authorizeInbound` / `prepareSession` /
`dispatchInbound`.
🔴 **Отличие от telegram:** `mattermost.InboundMessage` **не содержит** поля `Post` (есть
`Text`, `SenderID`, `SenderName`, `PostID`, `ReceivedAt`), а `NormalizeInbound` принимает
`Post` — поэтому `mattermost.Post` собирается руками.
### 2. `auth` — поддержка `mattermost:` subject (дельты 17, 18)
| Файл | Изменение | Почему |
|---|---|---|
| `auth/channel_token.go` | `ChannelMattermost = "mattermost"` + добавлен в список bind-токенов | иначе MM не получает owner-bind токен |
| `auth/canonical_users.go` | ветка `case ChannelMattermost` | 🔴 без неё `canonicalSubject` → `unsupported transport "mattermost"` → **все проверки авторизации молча false** |
| `auth/owner.go` | `MattermostSubject(userID string)` | рядом с `Telegram/Zulip/SlackSubject` |
| `channel/mattermost/locator_codec.go` | `ChannelIDOf(locator)` | достаёт `channel_id` из `AddressJSON` |
📌 `userSubject()` в `actors/command/start/handler.go` **уже** выдавал `mattermost:<id>` через
default-ветку — `/start` работает с MM без правок.
### 3. Регистрация в DI (дельта 19)
`handlersfx/module.go`: `fx.Annotate(newMattermostInboundHandler, fx.As(new(baldamattermost.InboundProcessor)))`.
### 4. Quality gates и деплой
| Гейт | Результат |
|---|---|
| `go build ./...` | ✅ exit 0 |
| `go vet ./...` | ✅ чисто |
| `go tool go-arch-lint check` | ✅ **OK - No warnings found** |
| Тесты проекта | ⚠️ 11 предсуществующих фейлов в `cmd/balda` / `internal/apps/balda` (workspace/plugin/session-memory) — **проверено `git stash` моей правки, падают и без неё**, к MM не относятся |
| Образ | ✅ `balda-mm:0.1.5`, собран на Mac, задеплоен, Up без ошибок старта |
### 5. Доказательство, что хендлер вызывается
```json
{"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` и отбился **на авторизации** — это корректное поведение.
Раньше на этом месте была тишина.
---
## 🔴 Единственный блокер: привязка владельца
Авторизация Balda — **subject-based**: владелец в `canonical` store привязан строкой
`<channel>:<principal>`, для MM это `mattermost:<mm_user_id>`.
**Состояние** (`strings` по `state.db`, т.к. образ `scratch` — ни shell, ни sqlite3):
```
balda_user_bindings → ПУСТА, ни одной привязки канала
admin user_id → b6517e56-1110-4cb9-a301-302c585fbd0d (administrator, active)
balda.appowner_auth_token → 32-символьный токен в kv-таблице
```
**Путь привязки:** `/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>`.
Получить токен:
```bash
ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | grep -A2 appowner_auth_token' </dev/null
```
⚠️ Значение токена в доке **не хранится** — только способ получения.
**После привязки:** `@balda` в `balda-test`, приёмка — **ответ виден в UI Mattermost**.
---
## Текущее состояние (2026-09-26, обновлено)
| Компонент | Статус |
|---|---|
| Бот `balda` в MM | ✅ создан, токен рабочий, в команде и канале |
| Образ | ✅ `balda-mm:0.1.5` (актуальный), задеплоен |
| Контейнер `balda-mm` | ✅ Up, без ошибок старта |
| Провайдер DeepSeek | ✅ `balda provider runtime ready` agent=deepseek |
| **WebSocket к Mattermost** | ✅ 101 Switching Protocols, `pong seq_reply` растёт, `bad_seq` — 0 |
| Причина «челлендж» (дельта 14) | ✅ исправлена |
| Причина «bad_seq» (дельта 16) | ✅ **исправлена, проверена** |
| Причина «Processor==nil» (дельты 17-19) | ✅ **исправлена, доказана логом** |
| MM-хендлер `handlersfx/mattermost_ingress.go` | ✅ написан, подключён в DI |
| Quality gates (build/vet/arch-lint) | ✅ все зелёные |
| **Привязка владельца** | 🔴 **НЕ ВЫПОЛНЕНА** — блокирует приёмку |
| **Ответ бота на `@balda`** | 🔴 пока нет — упирается в привязку владельца |
| Push-уведомления | ⬜ не начато |
⚠️ **Приёмка = ответ бота виден в UI Mattermost**, не «контейнер Up» и не «лог said accepted».
⚠️ Не докладывать «готово», пока сквозной путь не работает.
---
## Команда Alex (2026-09-26)
> «Ок, разворачивай бота на кракене с привязкой к ММ. Токен deepseek возьми скриптом из
@@ -22,28 +183,6 @@
---
## Текущее состояние (2026-09-26)
| Компонент | Статус |
|---|---|
| Бот `balda` в MM | ✅ создан, токен рабочий, **в команде и канале** |
| Образ `balda-mm:0.1.2` (linux/arm64) | ✅ собран на Mac, загружен на Kraken, задеплоен |
| Контейнер `balda-mm` | ✅ Up |
| Провайдер DeepSeek | ✅ `balda provider runtime ready` agent=deepseek |
| **WebSocket к Mattermost** | ✅ `mattermost websocket connected`, 101 Switching Protocols |
| Backoffice-админ | ✅ bootstrapped |
| Команда/канал в MM | ✅ team `qentra`, channel `balda-test`, пользователь `testuser` |
| Тишина ингресса (челлендж) | ✅ **исправлено** — дельта 14, образ `0.1.2` |
| **Ответ бота на `@balda`** | 🔴 **НЕТ ОТВЕТА** — событие теряется до `handleFrame` |
```
{"component":"balda.channel.mattermost.ingress","url":"ws://mattermost:8065/api/v4/websocket",
"message":"mattermost websocket connected"}
{"message":"Balda bot started. Press Ctrl+C to stop."}
```
---
## 🔴 Найден и исправлен дефект Фазы 2: объявленная команда без actor-хендлера
Первый запуск дал **crash-loop**:
@@ -317,9 +456,10 @@ ssh kraken 'cd /home/kraken/balda/secrets && TOK=$(cat mm-bot-token) && \
📌 **Правило:** если сокет аутентифицирован заголовком на dial — **не** слать
`authentication_challenge`. Одно из двух, не оба.
### Причина 3 — событие приходит, но теряется между фреймом и `handleFrame` (ОТКРЫТО)
### Причина 3 — ❌ ВЕРСИЯ ОПРОВЕРГНУТА (это был не обрыв в ингрессе)
После фикса (образ `0.1.2`, челленджа нет) картина:
Прошлая сессия предполагала, что событие теряется между `readLoop` и `handleFrame`.
Наблюдения, которые к этому привели:
| Проверка | Результат |
|---|---|
@@ -331,15 +471,20 @@ ssh kraken 'cd /home/kraken/balda/secrets && TOK=$(cat mm-bot-token) && \
| `bot_username: "balda"` в `config.yaml` | ✅ присутствует |
| `PostedData.Post` — тип `string`, JSON-строка поста | ✅ парсинг корректен |
**Вывод:** обрыв между получением фрейма в `readLoop` и входом в `handleFrame`.
Версии:
- **A** — фрейм не доходит до `handleFrame` (напр. `frame.messageType != websocket.TextMessage`);
- **B** — `handleFrame` вызван, но `json.Unmarshal(payload, &WebSocketEvent)` падает, и уходит
только `debug`-строка «ignoring undecodable mattermost websocket frame» (мог не заметить в потоке).
🔴 **Версия «обрыв между `readLoop` и `handleFrame`» НЕ подтвердилась.** Диагностический лог
каждого кадра (дельта 15) показал: фреймы `posted` **вообще не приходят** — не потому что
теряются, а потому что **MM перестал их слать**.
**Следующий шаг:** временный debug-лог в начало `handleFrame` (`ingress.go`), фиксирующий **каждый**
полученный фрейм → пересобрать `0.1.3` → отправить `@balda` от `testuser` → прочитать.
Даёт точку обрыва за один прогон.
Ключевая ошибка рассуждения: пробник подключался **своим отдельным сокетом** и события
получал; сокет же Балды к тому моменту был уже «отравлен» `bad_seq` и MM на него не пушил.
Отсюда ложный вывод «событие есть, но теряется внутри».
**Настоящие причины — см. §«Причина 3 (настоящая) — `bad_seq`» и §«Причина 4 — `Processor == nil`»
в начале этого дока.**
📌 **Урок:** «событие видно пробником» ≠ «событие доходит до приложения». Проверять **тот же
самый сокет**, а не параллельный. Для этого и понадобился диагностический лог каждого кадра
в самом ингрессе — он и вскрыл `bad_seq`.
---
@@ -375,12 +520,22 @@ ssh kraken 'cd /home/kraken/balda/secrets && TOK=$(cat mm-bot-token) && \
---
## Что осталось — 🔴 бот не отвечает
## Что осталось — 🔴 привязка владельца
Актуальное состояние обрыва — см. §«Причина 3» выше. Кратко: событие `posted` **доказанно
приходит** на сокет бота (поймано пробником с human `user_id`), все фильтры `inbound.go`
проверены и корректны, но Балда не логирует ничего даже на `debug`. Обрыв — между `readLoop`
и `handleFrame`.
~~Актуальное состояние обрыва — см. §«Причина 3» выше.~~ **Ингресс больше не блокер:**
обе причины тишины устранены, MM-хендлер написан и доказанно вызывается (см. начало дока).
Осталось **одно действие**: привязать владельца к MM, иначе авторизация отбивает посты
(`settlement_stage="unauthorized"`). Таблица `balda_user_bindings` пуста.
```bash
# 1. достать owner_auth_token
ssh kraken 'sudo -n strings /srv/…-49e8f586…/docker/balda/state/state.db | grep -A2 appowner_auth_token' </dev/null
# 2. привязать через DM: /start owner=<token>
ssh kraken '/tmp/mm-owner-bind.sh testuser "T3stPassw0rd!x" <owner_auth_token>'
# 3. проверка сквозного пути
ssh kraken '/tmp/mm-post-user.sh 7bjw4g7o1jdsbmjbqd649xxgaw testuser "T3stPassw0rd!x" "@balda скажи одно слово"'
```
⚠️ **Приёмка — ответ бота виден в UI Mattermost**, не «контейнер Up». Пока НЕ выполнена.
⚠️ Не докладывать «готово», пока сквозной путь не работает (см. реакцию Alex выше).