[2026-09-26] eagle: personal/projects/balda/balda-mattermost-connector.md personal/projects/balda/balda-mm-delegation-plan.md
This commit is contained in:
1 parent
88782f6ad7
commit
47ff9bfacd
2 files changed
+143
-48
No files matched your search
@@ -149,24 +149,40 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
|
||||
(3 метода: `ProcessInbound`, `HandleCommand`, `HandleUnsupportedCommand`),
|
||||
`SupportedCommands() []string`, `commandSupported(name)`.
|
||||
|
||||
### `server.go` — ingress
|
||||
### `ingress.go` — ingress (фактически реализовано)
|
||||
|
||||
⚠️ **Zulip использует outgoing webhook по HTTP.** У MM для ботов — **WebSocket**
|
||||
(`wss://<host>/api/v4/websocket`, `Sec-WebSocket-Protocol: bearer <token>`, порт 443/8065).
|
||||
Это отличается от Zulip-образца принципиально — нужен WS-клиент с reconnect.
|
||||
(`wss://<host>/api/v4/websocket`). Это отличается от Zulip-образца принципиально.
|
||||
|
||||
Обязательно перенести из Zulip-сервера:
|
||||
- **семафор на конкурентную обработку** (`processSem chan struct{}`, Zulip — 16) + `WaitGroup`;
|
||||
при заполнении отвечать «busy»/дропать, а не копить;
|
||||
- **`defer recover()`** вокруг обработки сообщения → не ронять процесс;
|
||||
- **таймаут обработки** (`zulipWebhookProcessingTimeout` = 5 мин) через
|
||||
`context.WithTimeout(context.WithoutCancel(...))`;
|
||||
- **фильтр эхо-сообщений бота** (`IsBotEcho`) — иначе бесконечный цикл;
|
||||
- **не превышать лимит тела** (1 MiB) — для MM это актуально для file-attachments;
|
||||
- graceful `Stop()` с ожиданием активной обработки (`waitForWebhookProcessing`);
|
||||
- все HTTP-таймауты заданы явно (`ReadHeaderTimeout`/`ReadTimeout`/`WriteTimeout`/`IdleTimeout`);
|
||||
**Как реализована аутентификация (проверено компиляцией):**
|
||||
- dial с заголовком `Authorization: Bearer <token>`;
|
||||
- **плюс** обязательный первый кадр `{"seq":1,"action":"authentication_challenge","data":{"token":"<token>"}}` —
|
||||
без него MM не начинает слать события;
|
||||
- `WebSocketURL()` в `client.go` сам конвертирует `http→ws` / `https→wss` и добавляет `/api/v4/websocket`.
|
||||
|
||||
⚠️ В ранних заметках было `Sec-WebSocket-Protocol: bearer <token>` — **это не то, что реализовано
|
||||
и не то, что описано в актуальных доках MM.** Использовать `authentication_challenge`.
|
||||
|
||||
Перенесено из Zulip-сервера и работает:
|
||||
- **семафор на конкурентную обработку** (`processSem chan struct{}`, 16) + `processWG`;
|
||||
при заполнении — дроп с warn-логом, не накопление;
|
||||
- **`defer recover()`** вокруг обработки поста → не ронять процесс;
|
||||
- **фильтр эхо-сообщений** (`IsBotEcho`) → нет бесконечного цикла;
|
||||
- **`SetReadLimit(1 MiB)`** на WS-кадр;
|
||||
- **reconnect-loop** с backoff 5 с — `readLoop` переустанавливает соединение, пока контекст жив;
|
||||
- **отдельная reader-горутина**: блокирующий `ReadMessage` не прерывается контекстом, поэтому
|
||||
`Stop()` закрывает conn, и горутина выходит по ошибке;
|
||||
- graceful `Stop()` с ожиданием активной обработки (`waitForProcessing`);
|
||||
- `Start`/`Stop` в `appports.TransportLifecycleStage`.
|
||||
|
||||
**Специфика MM, которой не было в Zulip:**
|
||||
- В **канале** Балда отвечает только на `@mention` (`MentionsBot`/`StripMention`); в DM — на всё.
|
||||
- События `post_edited` / `post_deleted` **осознанно игнорируются**: Балда действует по исходной
|
||||
инструкции и не переписывает свой ответ задним числом.
|
||||
- Системные посты (`Type != ""`) и удалённые отсеиваются до семафора.
|
||||
- `data.post` в событии `posted` — это **JSON-строка**, а не объект (нужен двойной `Unmarshal`).
|
||||
- Каналы типа `D` / `G` — это DM/групповой чат, а не канал команды.
|
||||
|
||||
### `presentation/` + `formatting.go`
|
||||
|
||||
- `formatting.go`: `FormattingPromptRuleAndExample() (rule, example string)` —
|
||||
@@ -206,6 +222,80 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
|
||||
|
||||
---
|
||||
|
||||
## Что фактически написано (2026-09-26)
|
||||
|
||||
Всё в `~/Developer/balda` на ветке **`feat/mattermost-channel`**. `go build ./...` = exit 0.
|
||||
|
||||
### Новый пакет `internal/apps/balda/channel/mattermost/`
|
||||
|
||||
| Файл | Строк | Содержимое |
|
||||
|---|---|---|
|
||||
| `client.go` | ~560 | REST v4: `CreatePost`, `UpdatePost`, `GetPost`, `GetChannel`, `GetUser`, `GetMe`, `GetTeamByName`, `GetChannelByName`, `GetDirectChannel`, `UploadFile`, `CreatePostWithFiles`, `AddChannelMember`, `Ping`; `WebSocketURL()`, `APIBaseURL()`, `ValidateConfig()`; `APIError` |
|
||||
| `locator_codec.go` | ~250 | `ChannelType="mattermost"`, префикс `mm`. AddressKey: **`c:<ch>` / `c:<ch>:<root>` / `d:<ch>`**. `NewChannelLocator(teamID, channelID, rootID)`, `NewDMLocator(channelID, userID)`, `DecodeLocator`, `LocatorFromAddressKey`, `ClassifyLocatorScope`, `ChannelIDFromLocator`, `RootIDFromLocator`, `IsDirectLocator`, `UserID`/`ParseUserID` |
|
||||
| `inbound.go` | ~250 | `InboundMessage`/`InboundCommand`/`InboundProcessor` (3 метода), `NormalizeInbound`, `LocatorForPost`, `IsDirectChannelType` (`D`/`G`), `IsBotEcho`, `IsSystemPost`, `IsDeletedPost`, `StripMention`, `MentionsBot`, `ParseCommand`, `SupportedCommands`, `WebSocketEvent`/`PostedData` |
|
||||
| `adapter.go` | ~420 | Все **9** `deliverycmd.Operation`: Plain, Markdown, AgentReply, Draft, Typing, Progress, **ClearQuestionControls, Photo, Document**. `sendWithPlainFallback`, `MarkdownPlainText`, `sendMedia` |
|
||||
| `media.go` | ~45 | `openMediaFile` (лимит 100 МБ), `mediaBaseName` |
|
||||
| `ingress.go` | ~380 | **WebSocket ingress**: dial с `Authorization: Bearer`, `authentication_challenge`, reconnect-loop, ping 30 с, `processSem` (16), `processWG`, `defer recover()` |
|
||||
| `formatting.go` | ~14 | `FormattingPromptRuleAndExample()` |
|
||||
| `presentation/presentation.go` | ~110 | `RenderLocator`, `RenderGoalProgress`, `RenderGoalOutcome` |
|
||||
| `mattermostfx/module.go` | ~100 | 7 групп DI (как `zulipfx`) |
|
||||
| `mattermostfx/config.go` | ~25 | `NewClient` + валидация при `enabled` |
|
||||
| `mattermostfx/ingress.go` | ~40 | `IngressParams` (`fx.In`), `NewIngress` |
|
||||
| `mattermostfx/structured.go` | ~100 | prompt contribution + 3 structured registrar |
|
||||
|
||||
### Изменения в shared-слое (все применены)
|
||||
|
||||
| Файл | Что добавлено |
|
||||
|---|---|
|
||||
| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` |
|
||||
| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"`, маршрут `{TransportMattermost, DeliveryFormatNone, NamePlainText}` в `BuiltinRoutes()` |
|
||||
| `turncmd/turn.go` | `SourceMattermost = "mattermost"` |
|
||||
| `internal/apps/balda/app.go` | `mattermostfx.Module` в список модулей (строка ~667) |
|
||||
|
||||
### Отклонения от плана разведки (важно)
|
||||
|
||||
1. **`server.go` → `ingress.go`.** Планировался `server.go` по образцу Zulip, но у MM нет
|
||||
HTTP-вебхука — это долгоживущее WS-соединение, а не `http.Server`. Имя `ingress.go`
|
||||
точнее отражает суть.
|
||||
2. **`SendDraftPlain` — не no-op.** В плане было «для MM это не no-op, edit-in-place есть».
|
||||
По факту: `OperationDraft` **отправляется как обычный пост** (текст не теряется), а
|
||||
edit-in-place живёт в `OperationClearQuestionControls` → `PUT /posts/{id}` — там он
|
||||
семантически уместен (гашение кнопок после выбора).
|
||||
3. **AddressKey не `t:<root_id>`.** Планировалось `c:` / `dm:` / `t:`. По факту тред кодируется
|
||||
**тем же channel-ключом с суффиксом**: `c:<ch>` (канал) и `c:<ch>:<root>` (тред). Причина:
|
||||
тред MM — это root-post внутри канала, и локатор должен оставаться стабильным для всего треда.
|
||||
4. **`ProgressPolicy{Typing: false}`.** Подтверждено: у MM-ботов **нет typing API**
|
||||
(`SendTyping` возвращает `nil` осознанно — иначе шум на каждом ходу).
|
||||
5. **`gorilla/websocket v1.5.3` был `// indirect`** в `go.mod` — стал прямым при сборке.
|
||||
6. **`commandSupport` переименован** в экспортируемый `IngressCommandSupport` (нужен для `fx.In`),
|
||||
оставлен алиас.
|
||||
|
||||
### 🔴 Известный незакрытый шаг
|
||||
|
||||
`internal/apps/balda/app.go` — модуль уже в списке, но **импорт не добавлен**:
|
||||
```go
|
||||
"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx"
|
||||
```
|
||||
Вставить рядом с `zulipfx` (строки 27). LSP: `undefined: mattermostfx` на строке 667.
|
||||
**Без этого `go build ./...` не пройдёт** — проверять первым делом.
|
||||
|
||||
### Конфиг, который нужен для запуска
|
||||
|
||||
Ключи читаются через `fx.ParamTags` / `name:"…"`:
|
||||
|
||||
```
|
||||
balda_mattermost_enabled
|
||||
balda_mattermost_server_url
|
||||
balda_mattermost_token
|
||||
balda_mattermost_bot_user_id
|
||||
balda_mattermost_bot_username
|
||||
```
|
||||
Соответствие в `config.yaml` (по образцу `balda.zulip.*`): `balda.mattermost.{enabled,server_url,token,bot_user_id,bot_username}`.
|
||||
Конкретные имена ключей yaml **проверить в `docs/reference/configuration.md`** перед Фазой 3 —
|
||||
я их не сверял, только зафиксировал DI-теги.
|
||||
|
||||
---
|
||||
|
||||
## Объём работ
|
||||
|
||||
Оценка из плана: **2-4 дня**. Разбивка:
|
||||
|
||||
Reference in new issue
Block a user