[2026-09-28] eagle: personal/plans/balda-mm-pr8-review-fixes.md
This commit is contained in:
1 parent
c43eb1e751
commit
d6c3ef4ffa
1 file changed
+197
-178
@@ -1,213 +1,232 @@
|
||||
# Balda — PR #8 review fixes: slash commands, /locator, group DM session
|
||||
|
||||
> Создан 2026-09-28. Статус: **ПЛАН — ожидает подтверждения Alex.**
|
||||
> Источник: ревью Alex на `baldaworks/balda#8` (Mattermost transport).
|
||||
> Часть [[personal/plans/balda-mm-pr-plan]] (PR #8). Ветка **`feat/mattermost-channel`**.
|
||||
> Ветки / кода ещё не трогал — только чтение и сверка.
|
||||
> Создан 2026-09-28. Статус: **✅ РЕАЛИЗОВАНО, ЗАПУШЕНО, CI ИДЁТ.**
|
||||
> Коммит **`cd4ce4bc`** в `feat/mattermost-channel` → PR **baldaworks/balda#8** обновлён.
|
||||
> 46 файлов, +6760/−27, база `0 позади / 8 впереди`.
|
||||
> Источник: ревью Alex на PR #8 + **четвёртый дефект, найденный мной** (§5).
|
||||
> Часть [[personal/plans/balda-mm-pr-plan]] (PR #8).
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR
|
||||
## 0. Итог
|
||||
|
||||
Ревью право по всем трём пунктам — **проверено в коде**, не на слово.
|
||||
Ревью Alex подтверждено в коде — все три пункта. Плюс найден **четвёртый дефект**,
|
||||
которого в ревью не было (§5). Все четыре закрыты одним коммитом.
|
||||
|
||||
| # | Дефект | Подтверждение в коде (коммит `9f1e6c9c`) |
|
||||
| # | Дефект | Статус |
|
||||
|---|---|---|
|
||||
| Б1 | Slash-команды не доходят: `/start` и прочие обрабатываются только из WS-события `posted` | `channel/mattermost/ingress.go` — `switch event.Event` знает только `posted`; отдельного HTTP-приёмника slash нет вовсе. У Slack он есть: `slackagent/server.go` → `mux.HandleFunc(commandsPath, h.handleCommands)` |
|
||||
| Б2 | `/locator` всегда падает: `locatorref.Parse` не знает `mattermost` | `locatorref/locatorref.go:52-62` — `switch channelType` покрывает только `telegram` / `zulip` / `slackagent`; ветки `mattermost` нет → `unsupported channel type` |
|
||||
| В3 | Групповой DM расщепляется по отправителям | `locator_codec.go:102-118` — `dmAddressKey` = `d:<ChannelID>` (один чат = один ключ), а `dmSessionID` при непустом `UserID` = `mm-dm-<hash(UserID)>` (**автор**, не чат) |
|
||||
|
||||
🔴 **Ключевой вопрос по Б1 — не «как», а «что именно».** См. §2: две реализации, они
|
||||
архитектурно разные. Нужно решение Alex **до** написания кода.
|
||||
| Б1 | Slash-команды не доходят (только WS `posted`) | ✅ HTTP-приёмник `command_server.go` |
|
||||
| Б2 | `/locator` падает: `locatorref` не знает `mattermost` | ✅ ветка + декодер + тесты |
|
||||
| В3 | Групповой DM расщепляется по авторам | ✅ свой тип/префикс/сессия + scope |
|
||||
| **Б4** | **Регистрация в `ScopeResolver` отсутствовала В PR** | ✅ `app.go` + `preflight.go` |
|
||||
|
||||
---
|
||||
|
||||
## 1. Сверка с докой (что именно было заявлено и оказалось неправдой)
|
||||
## 1. Гейты — все зелёные (локально, на закоммиченном дереве)
|
||||
|
||||
`docs/commands.md:20-22` (коммит `58f27120`) утверждает:
|
||||
```
|
||||
gofmt (мои файлы) → чисто
|
||||
go build ./... → exit 0
|
||||
go vet ./... → exit 0
|
||||
go tool golangci-lint run → 0 issues
|
||||
go tool go-arch-lint check --project-path . → OK - No warnings found (без runtime-local)
|
||||
go test -race (7 затронутых пакетов) → все ok
|
||||
go test ./cmd/balda/ (fx-граф) → ok
|
||||
```
|
||||
|
||||
> Mattermost uses one slash command per action, such as `/locator`, and accepts
|
||||
> them in direct messages and in channels where the bot is mentioned.
|
||||
**Новые тесты: 19** (`command_server_test.go` 15 + `locatorref/mattermost_test.go` 4).
|
||||
|
||||
И таблица (`docs/commands.md:26+`) ставит **`/help`, `/topic`, `/goalkeeper`, `/auto`,
|
||||
`/usage`, `/close`, `/cancel`, `/user` → «yes»** для Mattermost.
|
||||
⚠️ **`go-arch-lint`:** 28 замечаний «not attached» — **все** из `runtime-local/`
|
||||
(наша локальная папка с форком runtime, в PR её нет). Проверено `mv runtime-local` →
|
||||
`OK - No warnings found`. Это не наши правки.
|
||||
|
||||
⚠️ Всё это **ложь текущей реализации** — ни одна slash-команда в MM не обрабатывается.
|
||||
`/start` в таблице стоит `no`, но это тоже неверно: в MM `/start` — единственный
|
||||
документированный путь первичной привязки владельца, и он не работает.
|
||||
|
||||
📌 **Правка доков — обязательная часть фикса, а не «потом».** Колонка Mattermost в
|
||||
таблице доступности должна стать следствием кода, который реально есть.
|
||||
⚠️ **gofmt:** в репо 80 файлов не отформатированы — **унаследовано**, проверено на `HEAD`
|
||||
(`git show HEAD:<file> | gofmt -l /dev/stdin`). Мои файлы чисты. Свои gofmt-правки в
|
||||
`adapter.go`/`ingress_test.go` **откатил**, чтобы не раздувать diff PR-ом чужих файлов.
|
||||
|
||||
---
|
||||
|
||||
## 2. Б1 — slash-команды. Решение: вариант A (HTTP-приёмник). Вопрос снят.
|
||||
## 2. Б1 — slash-команды: HTTP-приёмник (вариант A)
|
||||
|
||||
> **Решение Alex 2026-09-28:** «Тебе что, не очевидно в прод-качестве?» — выбора нет.
|
||||
> B (убрать slash из доки) = **откат функциональности**, а не фикс. Прод-качество = A.
|
||||
> Спрашивать было нечего. Ниже сохранён разбор обоих путей как обоснование.
|
||||
📌 **Факт из доки MM:** «if you type in a slash command it will execute **without posting
|
||||
a message**» — WS-события `posted` для slash не будет. Ревью право на 100%.
|
||||
|
||||
Mattermost **не публикует** slash-команды как посты (в отличие от Telegram/Zulip) — поэтому
|
||||
текущий WS-путь их физически не видит. Есть два штатных механизма, и они взаимоисключающие
|
||||
по архитектуре:
|
||||
**Что уже было и что добавлено:**
|
||||
|
||||
📌 **Факт, подтверждённый докой MM** (`docs.mattermost.com/integrations-guide/slash-commands`):
|
||||
«if you type in a slash command it will execute **without posting a message**,
|
||||
whereas an outgoing webhook is only triggered by posted messages». То есть WS-события
|
||||
`posted` для slash **не будет** — ревью право на 100%.
|
||||
|
||||
🔴 **Что уже работает и что отсутствует — точно:**
|
||||
| Компонент | Статус |
|
||||
|---|---|
|
||||
| `mattermostInboundHandler.HandleCommand` (`handlersfx/mattermost_ingress.go`) — публикация в `commandcmd` | ✅ **написан и корректен** |
|
||||
| `ParseCommand` (`inbound.go:223`) — разбор `/name args` из текста | ✅ написан |
|
||||
| вызов `ParseCommand` из `processPosted` (`ingress.go`) — **mention-путь** | ✅ работает |
|
||||
| **HTTP-путь для slash** (MM POST → наш эндпоинт) | ❌ **отсутствует целиком** |
|
||||
| `mattermostInboundHandler.HandleCommand` — публикация в `commandcmd` | ✅ уже был, корректен |
|
||||
| `ParseCommand` — разбор `/name args` | ✅ уже был |
|
||||
| вызов из `processPosted` (mention-путь: `@balda /locator`) | ✅ работает |
|
||||
| **HTTP-путь для slash** | ❌ **добавлен** `channel/mattermost/command_server.go` |
|
||||
|
||||
Значит фикс Б1 — **не переписывать command-машину**, а добавить к ней **второй вход**.
|
||||
Command-машина исправна; нет только способа донести до неё slash-запрос.
|
||||
🔴 **Правка Alex (важный урок):** «в твоей ММ реализации взят существующий slack транспорт
|
||||
за основу? Они должны быть практически идентичны». Первая версия разошлась со Slack в
|
||||
пяти местах **без причины** — переписал по образцу `slackagent/server.go`:
|
||||
|
||||
### Вариант A — Slack-style: исходящий webhook + HTTP-приёмник (ВЫБРАН)
|
||||
|
||||
Slash-команда MM настраивается на **Request URL** — MM делает `POST` в наш HTTP-эндпоинт.
|
||||
Это ровно то, что уже сделано для Slack, значит есть рабочий образец в репо.
|
||||
|
||||
Что дублируется из `slackagent/`:
|
||||
- HTTP-сервер с отдельным путём `/mattermost/commands` (`server.go:129`-аналог);
|
||||
- `handleCommands` → `decodeCommandRequestWithRegistry` → `commandcmd.Request` → common handler;
|
||||
- ответ в MM — JSON `{"response_type":"ephemeral"|"in_channel","text":"..."}` (MM-специфично,
|
||||
отличается от Slack `{"text":...}`);
|
||||
- **верификация**: у MM slash-команда подписывается полем `token` в теле запроса
|
||||
(не HMAC-подпись, как у Slack) — сравнение с токеном из конфига;
|
||||
- lifeзycle: тот же `appports.TransportLifecycleStage`, что у WS-ingress (либо расширить
|
||||
существующий `Ingress`, либо отдельный `Server` — см. §6).
|
||||
|
||||
Плюсы: `/start`, `/topic`, `/goalkeeper`, `/auto`, `/usage`, `/close`, `/cancel`, `/user`
|
||||
работают **в полном объёме**, как в доке и таблице. Минусы: нужен публичный HTTP-эндпоинт
|
||||
для MM и **новый ключ конфига** (slash-токен) + правки доков по настройке.
|
||||
|
||||
### Вариант B — «mention-форма»: команды только текстом в посте (ОТКЛОНЁН)
|
||||
|
||||
Оставить один WS-путь, а из доки/таблицы убрать slash-обещания: писать команды как
|
||||
`@balda /locator` (текст поста, `ParseCommand` их уже разбирает — `ingress.go`/`inbound.go`).
|
||||
|
||||
Почему отклонён: это **не фикс, а удаление уже обещанной функциональности**. `/start owner=<token>`
|
||||
в MM-развёртывании — штатный вход владельца, и `docs/mattermost.md:117-119` прямо описывает его
|
||||
как slash-команду в DM. Вариант B = переписать доку под урезанный код. Для «прод-качества» —
|
||||
неприемлемо.
|
||||
|
||||
### Итог
|
||||
|
||||
**A.** В доке уже обещаны все slash-команды, у Slack есть готовый образец
|
||||
(`slackagent/server.go:129` → `mux.HandleFunc(commandsPath, h.handleCommands)` + `handleCommands`
|
||||
→ `decodeCommandRequestWithRegistry` → `commandcmd.Request`), а `/start owner=<token>` — штатный
|
||||
вход владельца в MM-развёртывании.
|
||||
|
||||
---
|
||||
|
||||
## 3. Б2 — `/locator`: добавить `mattermost` в `locatorref`
|
||||
|
||||
Механически, по образцу `slackagentLocatorFromAddressKey` (там та же форма `c:`/`t:`):
|
||||
|
||||
| Что | Куда |
|
||||
|---|---|
|
||||
| ветка `case baldamattermost.ChannelType:` в `switch channelType` | `locatorref/locatorref.go:52-62` |
|
||||
| `mattermostLocatorFromAddressKey(addressKey string)` — разбор `c:<channel>`, `c:<channel>:<root>`, `d:<channel>` | рядом с `slackAgentLocatorFromAddressKey` |
|
||||
| конструкторы `NewMattermostChannelLocator` / `NewMattermostDMLocator` (по образцу `NewSlackAgent*`) | там же |
|
||||
| **импорт** `channel/mattermost` — ⚠️ проверить архитектурное правило | см. ниже |
|
||||
|
||||
🔴 **Питфолл архитектуры.** `AGENTS.md` прямо требует: «Keep `locatorref` **independent**
|
||||
from concrete transport adapter packages». Импорт `channel/mattermost` в `locatorref` это
|
||||
**нарушает**. Как это решено сейчас: `locatorref` импортирует `telegramref` (отдельный
|
||||
тонкий пакет-ref), а для zulip/slack — **дублирует константы строкой**:
|
||||
```go
|
||||
channelTypeZulip = "zulip"
|
||||
channelTypeSlackAgent = slackagentref.ChannelType // ← тонкий ref-пакет
|
||||
```
|
||||
📌 **Значит правильная форма для MM — тонкий пакет `mattermostref`** (или константа-строка
|
||||
в `locatorref`), **не** импорт транспорта. Решение сверить с `slackagentref`/`telegramref`,
|
||||
прежде чем писать код. Это тот случай, где `go-arch-lint` даст красное на наивной реализации.
|
||||
|
||||
**Ещё два следствия Б2, которые надо закрыть тем же изменением:**
|
||||
- `internalmcp/manager.go:409` — `locatorref.Parse(channelType + ":" + addressKey)`. Любой
|
||||
MM-таргет там сейчас падает.
|
||||
- `envelopetarget/resolve.go:62` — `locatorref.Parse(target.Key)`. То же самое →
|
||||
`balda.scheduler.jobs` с `target: locator` для MM **не работает**.
|
||||
|
||||
⚠️ Это и есть вторая половина фразы ревью «также исключает Mattermost из config targets,
|
||||
использующих тот же парсер». Правка только `handler.go` задачу не закрывает.
|
||||
|
||||
---
|
||||
|
||||
## 4. В3 — групповой DM: ключ чата вместо ключа автора
|
||||
|
||||
Сейчас:
|
||||
```go
|
||||
func dmAddressKey(a LocatorAddress) string { return fmt.Sprintf("d:%s", url.PathEscape(a.ChannelID)) } // ЧАТ
|
||||
func dmSessionID(a LocatorAddress) string {
|
||||
if a.UserID != "" { return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.UserID)) } // АВТОР ← баг
|
||||
return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.ChannelID))
|
||||
}
|
||||
```
|
||||
DM (`D`) приходит с `UserID` → сессия по автору. **Групповой DM (`G`) канал общий, а авторов
|
||||
много** → у каждого участника своя сессия. Ревью право.
|
||||
|
||||
Правильная форма — **различать `D` и `G` явно**, а не «если есть UserID»:
|
||||
|
||||
| Тип канала MM | AddressKey | SessionID |
|
||||
|---|---|---|
|
||||
| `D` (1:1) | `d:<channel_id>` | `mm-dm-<hash(channel_id)>` (чат = 2 участника, стабильно) |
|
||||
| `G` (групповой) | `d:<channel_id>` | `mm-gdm-<hash(channel_id)>` — **по чату** |
|
||||
|
||||
⚠️ Соображения, которые надо учесть при реализации:
|
||||
- **Обратная совместимость:** существующие 1:1 DM-сессии на Kraken имеют id
|
||||
`mm-dm-<hash(UserID)>` (перепривязка сломает их → новые сессии, старые «осиротеют»).
|
||||
Либо принять (тестовая инсталляция), либо оставить для `D` форму по `UserID`.
|
||||
- `IsDirectChannelType` (`inbound.go`) уже знает `D`/`G` — переиспользовать.
|
||||
- Точка входа — `LocatorForPost` (`inbound.go`), она и должна различать `D`/`G`.
|
||||
- Правка ведёт к обновлению `locator_codec_test.go` (тест на изоляцию thread↔channel
|
||||
придётся дополнить тестом «два автора в `G` → одна сессия»).
|
||||
|
||||
---
|
||||
|
||||
## 5. Порядок работ (порядок важен: сначала решается A/B)
|
||||
|
||||
| # | Шаг | Файл | Тип |
|
||||
| Что | Slack | Было у меня | Стало |
|
||||
|---|---|---|---|
|
||||
| 0 | **Решение Alex: вариант A или B для Б1** | — | ⛔ блокирует всё остальное |
|
||||
| 1 | Б2: `mattermost` в `locatorref` (через ref-пакет, не импорт транспорта) | `locatorref/`, новый `mattermostref`? | код |
|
||||
| 2 | Б2-хвост: тесты на `internalmcp/manager.go` и `envelopetarget/resolve.go` путях | тесты | код |
|
||||
| 3 | В3: `D`/`G` различать в `dmSessionID` + `LocatorForPost` | `locator_codec.go`, `inbound.go` | код |
|
||||
| 4 | В3: тест «два автора в групповом DM → одна сессия» | `locator_codec_test.go` | тест |
|
||||
| 5 | Б1 (если A): HTTP-приёмник slash + `token`-верификация + DI + конфиг-ключ | `channel/mattermost/`, `mattermostfx/`, `app.go`, `balda.yaml` | код |
|
||||
| 6 | Б1 (если A): `docs/mattermost.md` — настройка slash Request URL и токена | доки | доки |
|
||||
| 7 | Правка таблицы команд `docs/commands.md` — колонка Mattermost = **реальность**, не обещание | доки | доки |
|
||||
| 8 | Гейты: `go build` / `vet` / `golangci-lint` / `go-arch-lint` / `go test -race` | — | проверка |
|
||||
| 9 | Push в `feat/mattermost-channel` → **дождаться CI 8/8** | — | проверка |
|
||||
| 10 | Обновить Obsidian: [[personal/plans/balda-mm-pr-plan]], [[personal/projects/balda/balda-mattermost-connector]] | доки | доки |
|
||||
| `invocationID` | хеш от тела | из `post_id` | `commandInvocationID(body)` |
|
||||
| таймаут команды | `2500ms` | `30s` | `2500ms` |
|
||||
| семафор | `processSem` 16 | отсутствовал | 16 |
|
||||
| `webhookMaxBodyBytes` | `1 << 20` | `64 << 10` | `1 << 20` |
|
||||
| `ReadTimeout`/`Idle` | 10s / 30s | 15s / 60s | 10s / 30s |
|
||||
|
||||
📌 Коммиты — Conventional Commits, правки — **дельта-апплаером** (`tools/run-project-deltas.sh`,
|
||||
`check` → `apply`), не sed/in-place (правило Alex).
|
||||
**Осталось ровно два отличия, и оба диктует провайдер (не наш выбор):**
|
||||
1. **Верификация:** у MM `token` в теле запроса (сравнение constant-time), у Slack —
|
||||
HMAC-подпись `X-Slack-Signature`. Копировать `verifySignature` нельзя.
|
||||
2. **Форма ответа:** MM `{"response_type":"ephemeral","text":"..."}`, Slack `{"text":...}`.
|
||||
|
||||
📌 Ветка та же (`feat/mattermost-channel`) — правки уедут в открытый PR #8 и **сбросят
|
||||
пройденный CI**. Это ожидаемо: PR не смержен, ревью просит исправлений.
|
||||
**Поведение:**
|
||||
- принимает обе конфигурации команды: `/locator` (аргументы в `text`) и `/balda locator`
|
||||
(первое слово `text` = подкоманда) — как у Slack с `/balda`;
|
||||
- неизвестная команда → **200 + usage** (не 400, не тишина) — тоже как у Slack;
|
||||
- тип канала (`D`/`G`/`O`/`P`) резолвится через `GetChannel`, т.к. slash его не несёт;
|
||||
при ошибке резолва — трактуем как **не-DM** (строгая авторизация);
|
||||
- отвечает **сразу**, результат уходит постом (MM показывает timeout автору за ~3 с).
|
||||
|
||||
**Новые ключи конфига** (`balda.mattermost.*`): `commands_enabled`,
|
||||
`commands_listen_addr` (`:8093`), `commands_path` (`/mattermost/commands`),
|
||||
`commands_token`. Все 4 + провайдеры `name:`-тегов в `app.go`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Риски / питфоллы, которые уже видны
|
||||
## 3. Б2 — `/locator`: `mattermost` в `locatorref`
|
||||
|
||||
1. 🔴 **`locatorref` не должен импортировать `channel/mattermost`** — `go-arch-lint` даст
|
||||
`composition_root`/boundary-ошибку. Форма фикса — тонкий ref-пакет, как `slackagentref`.
|
||||
2. 🔴 **Slash-команды MM требуют публичного HTTPS** для Request URL. Если стенд на Kraken
|
||||
без публичного адреса — нужен туннель либо вариант B. **Проверить до выбора A.**
|
||||
3. ⚠️ **MM slash-запросы возвращаются в течение 3 с**, иначе MM показывает timeout. Тяжёлые
|
||||
команды (`/goalkeeper`) обязаны отвечать сразу, а результат доставлять постом — как у Slack.
|
||||
4. ⚠️ **`token` в slash-запросе — не HMAC.** Другая модель верификации, чем у Slack; копировать
|
||||
`signedSlackRequest`-логику нельзя.
|
||||
5. ⚠️ **Обратная совместимость DM-сессий** (§4) — смена id осиротит существующие сессии.
|
||||
6. ⚠️ Изменение Б3 меняет поведение `transport_user_id` в persisted-метаданных сессии —
|
||||
перепроверить тест `TestLocatorForPostScopesReplyToThreadRoot` и «чужой user_id» из
|
||||
§«Два не-блокера» [[personal/plans/balda-mm-pr-plan]].
|
||||
Механически, по образцу `slackagentLocatorFromAddressKey`.
|
||||
|
||||
🔴 **Питфолл архитектуры ЗАКРЫТ правильно:** `AGENTS.md` требует независимости `locatorref`
|
||||
от транспортов. Не импортировал `channel/mattermost` — использовал
|
||||
`string(deliverycmd.ChannelTypeMattermost)`, ровно как для Slack
|
||||
(`channelTypeSlackAgent = string(deliverycmd.ChannelTypeSlackAgent)`).
|
||||
`deliverycmd` уже в allow-list `locatorref`. **`go-arch-lint` зелёный.**
|
||||
|
||||
🔴 **Ошибка, которую поймал тестами:** первая версия декодера использовала типы
|
||||
`"channel"`/`"direct"`, а транспорт определяет `"channel"`/`"dm"`/`"group"`. Несовпадение
|
||||
строк = локатор декодируется, но транспорт его отвергает. **Сверить имена типов с
|
||||
`locator_codec.go`, не выдумывать.**
|
||||
|
||||
**Хвост Б2 (закрыт тем же изменением):**
|
||||
- `internalmcp/manager.go:409` — `locatorref.Parse` теперь работает для MM;
|
||||
- `envelopetarget/resolve.go:62` — `balda.scheduler.jobs` `target: locator` для MM теперь
|
||||
работает. Это вторая половина фразы ревью «исключает Mattermost из config targets».
|
||||
|
||||
**Тесты:** `locatorref/mattermost_test.go` — 4 теста (канал/тред, DM/группа,
|
||||
Format-round-trip, отказ на кривом ключе).
|
||||
|
||||
---
|
||||
|
||||
## 4. В3 — групповой DM
|
||||
|
||||
**Было (дефект):**
|
||||
```go
|
||||
dmSessionID: if a.UserID != "" { return "mm-dm-<hash(UserID)>" } // АВТОР, не чат
|
||||
```
|
||||
`G`-канал общий, авторов много → у каждого своя сессия.
|
||||
|
||||
**Стало:**
|
||||
|
||||
| Тип MM | AddressKey | SessionID | Scope |
|
||||
|---|---|---|---|
|
||||
| `D` (1:1) | `d:<channel_id>` | `mm-dm-<hash(channel_id)>` | personal |
|
||||
| `G` (групповой) | **`g:<channel_id>`** | **`mm-gdm-<hash(channel_id)>`** | personal |
|
||||
|
||||
🔴 **Отдельный префикс ключа `g:` — не косметика.** Общий `d:` не различим при
|
||||
`LocatorFromAddressKey`: round-trip `d:` восстанавливал **любой** групповой DM как 1:1.
|
||||
Свой префикс сохраняет тип.
|
||||
|
||||
**Закрыто в 8 местах** (§«Тип канала» — самый коварный):
|
||||
`addressTypeGroupDM` · `NewGroupDMLocator` · `dmAddressKey` · `dmSessionID` ·
|
||||
`IsDirectLocator` · **`validateLocatorAddress`** · **`ClassifyLocatorScope`** ·
|
||||
`LocatorForPost` + `IsGroupChannelType`.
|
||||
|
||||
⚠️ **`validateLocatorAddress` и `ClassifyLocatorScope` — я их пропустил в первой версии.**
|
||||
`DecodeLocator` возвращает `ok=false` для неизвестного типа, и тогда **все**下游-хелперы
|
||||
fail-closed: `IsDirectLocator`, `ChannelIDFromLocator`, `ClassifyLocatorScope`.
|
||||
Тест `TestLocatorForPostUsesDirectLocatorForDMAndGroupChannels/G` это и поймал.
|
||||
|
||||
⚠️ **Обратная совместимость:** старая форма `D`-сессии была `mm-dm-<hash(UserID)>`, стала
|
||||
`mm-dm-<hash(channel_id)>` → существующие 1:1 DM-сессии на Kraken **осиротеют** (новые
|
||||
создадутся, старые останутся мёртвыми). Тестовая инсталляция — принято.
|
||||
|
||||
---
|
||||
|
||||
## 5. 🔴 Б4 — четвёртый дефект, которого НЕ было в ревью
|
||||
|
||||
**Регистрация MM в `ScopeResolver` отсутствовала в PR #8.**
|
||||
|
||||
Диагностика: `grep ClassifyLocatorScope internal/apps/balda/app.go` — строка **есть** в
|
||||
рабочем дереве, но `git show HEAD:...app.go | grep ClassifyLocatorScope` — строка
|
||||
**отсутствует**. Значит она была WIP-правкой на диске и в коммиты не уехала.
|
||||
В `preflight.go` её не было вообще (обе ветки).
|
||||
|
||||
**Почему это критично (питфолл #11 навыка):** `sessionmemoryapp.ScopeResolver` строит карту
|
||||
классификаторов по `channelType`; резолвер **fail-closed** (`capture.go`:
|
||||
`classifier == nil` → `CodeUnsupportedScope`). Симптом при `session_memory.enabled: true`:
|
||||
```
|
||||
warn … "no session-memory classifier is registered for \"mattermost\""
|
||||
→ в чате: "Could not reset this session."
|
||||
```
|
||||
+ **MCP-инструменты не подключаются** (`function_call_part_count: 0`).
|
||||
Компилируется, `go build`/`vet`/линтеры зелёные, отказ **только в рантайме**.
|
||||
|
||||
Зарегистрировано в **обоих** файлах (`app.go` и `preflight.go`) — карт **две**,
|
||||
правка одной даёт расхождение «preflight прошёл, рантайм упал».
|
||||
|
||||
📌 **Урок метода:** «зелёные гейты» ≠ «рабочий транспорт». Эта строка проходила все
|
||||
статическиe проверки, отсутствуя в PR. Сверять `git show HEAD:<file>` с рабочим деревом
|
||||
для критичных регистраций, а не доверять `git status`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Что понадобилось дополнительно (для будущих транспортов)
|
||||
|
||||
1. 🔴 **Каждый `name:`-тег в `mattermostfx` = обязательный провайдер в `app.go`.**
|
||||
Добавлены 4: `balda_mattermost_commands_{enabled,listen_addr,path,token}`.
|
||||
Иначе fx: `missing types: string[name="balda_mattermost_commands_enabled"]`.
|
||||
2. ⚠️ **Ключ обязан быть в `cmd/balda/balda.yaml`** — env-override
|
||||
(`BALDA_MATTERMOST_COMMANDS_*`) применяется только к leaf-ключам, **уже присутствующим**
|
||||
в YAML (`appconfig/loader.go`). Молчаливое игнорирование иначе.
|
||||
3. ✅ **DI-граф проверен реально** — `TestValidateCommandRunsValidationWithMattermostTransport`
|
||||
прогоняет `loadBaldaCommandConfig(true)` + `validateBaldaApplication(prepared)` с
|
||||
включённым MM. В логе видно оба провайдера: `NewCommandServer` + `NewIngress`.
|
||||
**`go build` этого не проверяет** — только `fx.ValidateApp`.
|
||||
|
||||
### Файлы (новые)
|
||||
| Файл | Строк |
|
||||
|---|---|
|
||||
| `channel/mattermost/command_server.go` | ~409 |
|
||||
| `channel/mattermost/command_server_test.go` | ~361 |
|
||||
| `locatorref/mattermost_test.go` | ~94 |
|
||||
|
||||
### Дельты (все применены, `tools/`)
|
||||
`deltas-pr8-b2-locatorref.json` · `-b2-locatorref-types.json` · `-p3-groupdm.json` ·
|
||||
`-p3-groupdm-keys.json` · `-b1-wiring.json` · `-b1-config-keys.json` · `-lint-fixes.json` ·
|
||||
`-docs-commands.json` · `-docs-mattermost.json`
|
||||
|
||||
---
|
||||
|
||||
## 7. Доки
|
||||
|
||||
- `docs/commands.md` — MM-колонка приведена к **реальности**: `/start`, `/topic`,
|
||||
`/goalkeeper`, `/auto`, `/usage`, `/close`, `/cancel`, `/user`, `/locator` = **yes**
|
||||
(было `no` — дока врала). Абзац про HTTP-природу slash + обе конфигурации команды.
|
||||
- `docs/mattermost.md` — новый раздел **Slash command setup** (Request URL, token, шаги),
|
||||
4 ключа в таблице, env-образец, slash-специфичный troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## 8. Дальше
|
||||
|
||||
| # | Шаг | Статус |
|
||||
|---|---|---|
|
||||
| 1 | Дождаться CI на `cd4ce4bc` (8 джобов) | ⏳ идёт |
|
||||
| 2 | Внешнее ревью PR #8 (`reviewDecision` пустой) | ⛔ внешний блокер |
|
||||
| 3 | Живая приёмка slash на Kraken (публичный HTTPS для Request URL!) | ⏳ после 1 |
|
||||
| 4 | Фаза 4 — исполнитель (делегирование) | 🎯 основной фронт |
|
||||
|
||||
⚠️ **Б1 требует публичного HTTPS** для Request URL slash-команды. Стенд Kraken —
|
||||
проверить достижимость эндпоинта **до** приёмки; иначе нужен туннель.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user