[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:
1 parent
bf7fa3569d
commit
6ae7f397ef
5 files changed
+688
-86
No files matched your search
@@ -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 выше).
|
||||
|
||||
Reference in new issue
Block a user