diff --git a/Getting-Started-RU.md b/Getting-Started-RU.md
index c73cf0e..2a7371d 100644
--- a/Getting-Started-RU.md
+++ b/Getting-Started-RU.md
@@ -6,7 +6,7 @@ English version: [[Getting-Started]]
## Что нужно сначала
-- Go 1.24 или новее
+- Go 1.26 или новее
- токен Telegram-бота от `@BotFather`
- Go-модуль, который может импортировать `git.scuroneko.dev/scuroneko/laniakea`
diff --git a/Getting-Started.md b/Getting-Started.md
index 4e42dc1..9edc1c8 100644
--- a/Getting-Started.md
+++ b/Getting-Started.md
@@ -7,7 +7,7 @@ Start here if you are integrating Laniakea into a new bot for the first time.
This page covers the shortest path to a working bot, the minimum concepts you need to understand, and the most important defaults that affect startup and runtime behavior.
## What you need first
-- Go 1.24 or newer
+- Go 1.26 or newer
- a Telegram bot token from `@BotFather`
- a module that can import `git.scuroneko.dev/scuroneko/laniakea`
diff --git a/Home-RU.md b/Home-RU.md
index ba66efc..396add4 100644
--- a/Home-RU.md
+++ b/Home-RU.md
@@ -40,6 +40,7 @@ English version: [[Home]]
## Миграция и сопровождение
- [[Migration-RU]]
+- [[V2-Migration-Plan-RU]] — DRAFT; предполагаемые breaking changes и compatibility bridges v1.2
- [[Semver-and-Releases-RU]]
- [[Framework-Backlog-RU]]
diff --git a/Home.md b/Home.md
index ea95b6b..0064bc9 100644
--- a/Home.md
+++ b/Home.md
@@ -42,5 +42,6 @@ Use this wiki as the structured companion to the README: start with setup, then
## Migration and Maintenance
- [[Migration]]
+- [[V2-Migration-Plan]] — DRAFT; proposed breaking changes and v1.2 compatibility bridges
- [[Semver-and-Releases]]
- [[Framework-Backlog]]
diff --git a/MessageContext-RU.md b/MessageContext-RU.md
index f7779b8..1cb4495 100644
--- a/MessageContext-RU.md
+++ b/MessageContext-RU.md
@@ -107,16 +107,16 @@ English version: [[MessageContext]]
## Rich-сообщения
-`RichAnswer(...)` и `RichAnswerKeyboard(...)` отправляют структурированные rich-сообщения Bot API 10.1, собранные из фрагментов `tgfmt`:
+`RichAnswer(...)` и `RichAnswerKeyboard(...)` валидируют и отправляют input rich-блоки Bot API 10.2, собранные через `tgrich`:
```go
ctx.RichAnswer(
- tgfmt.H1(tgfmt.NewRich("Отчёт")),
- tgfmt.P(tgfmt.NewRich("всё работает").Bold()),
+ tgrich.H1(tgrich.Text("Отчёт")),
+ tgrich.P(tgrich.Bold(tgrich.Text("всё работает"))),
)
```
-Полный DSL и приёмная сторона описаны на странице [[Rich-Messages-RU]].
+Все конструкторы, загрузка медиа, валидация и приёмная сторона описаны на странице [[Rich-Messages-RU]].
## Drafts и localization
diff --git a/MessageContext.md b/MessageContext.md
index a995e58..f52d4a1 100644
--- a/MessageContext.md
+++ b/MessageContext.md
@@ -113,16 +113,16 @@ This is useful for:
## Rich messages
-Use `RichAnswer(...)` and `RichAnswerKeyboard(...)` to send structured Bot API 10.1 rich messages built from `tgfmt` fragments:
+Use `RichAnswer(...)` and `RichAnswerKeyboard(...)` to validate and send Bot API 10.2 input rich blocks built with `tgrich`:
```go
ctx.RichAnswer(
- tgfmt.H1(tgfmt.NewRich("Report")),
- tgfmt.P(tgfmt.NewRich("all systems go").Bold()),
+ tgrich.H1(tgrich.Text("Report")),
+ tgrich.P(tgrich.Bold(tgrich.Text("all systems go"))),
)
```
-See [[Rich-Messages]] for the full DSL and the receive side.
+See [[Rich-Messages]] for all constructors, media uploads, validation, and the receive side.
## Markdown helpers
diff --git a/Migration-RU.md b/Migration-RU.md
index 715a668..f440fcc 100644
--- a/Migration-RU.md
+++ b/Migration-RU.md
@@ -12,6 +12,7 @@ English version: [[Migration]]
- [[Bot-Lifecycle-RU]]
- [[Commands-and-Plugins-RU]]
- [[Inline-Keyboards-and-Payloads-RU]]
+- [[V2-Migration-Plan-RU]] — DRAFT-план cleanup для v2 и compatibility bridges v1.2
- [[Semver-and-Releases-RU]]
## Что важно помнить при апгрейде
diff --git a/Migration.md b/Migration.md
index c10b416..a613e3d 100644
--- a/Migration.md
+++ b/Migration.md
@@ -12,6 +12,7 @@ Also see:
- [[Bot-Lifecycle]] for the current startup and shutdown model;
- [[Commands-and-Plugins]] for handler registration patterns;
- [[Inline-Keyboards-and-Payloads]] for payload-type behavior;
+- [[V2-Migration-Plan]] for the DRAFT v2 cleanup plan and v1.2 compatibility bridges;
- [[Semver-and-Releases]] for the project's versioning intent.
## Recommended upgrade strategy
diff --git a/Rich-Messages-RU.md b/Rich-Messages-RU.md
index 7b740a0..64412eb 100644
--- a/Rich-Messages-RU.md
+++ b/Rich-Messages-RU.md
@@ -1,71 +1,141 @@
# Rich-сообщения
-Rich-сообщения — механизм Bot API 10.1 для сильно структурированного текста: заголовки, абзацы, списки с чекбоксами, таблицы, медиа-коллажи, раскрывающиеся секции, математика и другое. Laniakea поддерживает их целиком: типизированный HTML-DSL для отправки, типизированные wire-структуры для приёма и стриминг черновиков.
+Rich-сообщения поддерживают заголовки, абзацы, списки, таблицы, медиа, цитаты, раскрывающиеся секции и формулы. Laniakea поддерживает исходящие деревья `InputRichBlock` из Bot API 10.2, входящие деревья `RichBlock`, преобразование в HTML, multipart-загрузки и потоковые черновики.
-## Ключевая асимметрия
+## Пакеты и поток данных
-Telegram сделал отправку и приём намеренно разными, и Laniakea повторяет это разделение:
+- `tgapi` содержит wire-типы Telegram и методы API.
+- `tgrich` содержит конструкторы `tgapi.RichText` и `tgapi.InputRichBlock`, валидацию и преобразование в HTML.
+- Исходящие сообщения используют `tgapi.InputRichMessage`. Должно быть задано ровно одно из полей `Blocks`, `HTML` или `Markdown`.
+- Входящие сообщения используют `Message.RichMessage` с узлами `tgapi.RichBlock` и `tgapi.RichText`.
-- **Отправляется разметка, а не дерево.** Единственный способ отправить rich-сообщение — `tgapi.InputRichMessage` со строкой `HTML` или `Markdown`. API для отправки дерева блоков не существует.
-- **Принимается дерево, а не разметка.** Входящее rich-сообщение приходит как `Message.RichMessage` — полностью типизированное дерево узлов `RichBlock` и `RichText`, собранное сервером.
-
-Поэтому в Laniakea:
-
-- **out**-сторона живёт в `tgfmt` (`rich.go`): типизированные HTML-фрагменты и конструкторы с чистыми именами (`P`, `H1`, `Bold`, `Photo`, ...);
-- **in**-сторона живёт в `tgapi` (`richtext.go`, `richblock.go`): wire-типы с именами официальных объектов API (`RichBlockParagraph` — это `RichBlockWrap{Tag: "paragraph"}`, `RichTextBold` — `RichTextWrap{Tag: "bold"}` и т.д.) плюс `UnmarshalRichMessage`.
+Исходящие и входящие типы блоков намеренно разделены. `InputRichBlock*` описывает данные, принимаемые Telegram, а `RichBlock*` — нормализованное дерево, возвращаемое Telegram.
## Отправка из обработчика
-`MessageContext.RichAnswer(...)` принимает фрагменты `tgfmt` и отправляет их через `sendRichMessage`:
+`MessageContext.RichAnswer` принимает input-блоки и собирает валидированное HTML rich-сообщение:
```go
-import "git.scuroneko.dev/scuroneko/laniakea/tgfmt"
+import (
+ "git.scuroneko.dev/scuroneko/laniakea/tgrich"
+)
func report(ctx *laniakea.MessageContext, _ struct{}) error {
ctx.RichAnswer(
- tgfmt.H1(tgfmt.NewRich("Дневной отчёт")),
- tgfmt.P(
- tgfmt.NewRich("Статус: "),
- tgfmt.NewRich("всё работает").Bold(),
- ),
- tgfmt.Ul(
- tgfmt.LiCheckbox(true, tgfmt.NewRich("бэкапы")),
- tgfmt.LiCheckbox(false, tgfmt.NewRich("миграция")),
+ tgrich.H1(tgrich.Text("Дневной отчёт")),
+ tgrich.P(tgrich.Concat(
+ tgrich.Text("Статус: "),
+ tgrich.Bold(tgrich.Text("всё работает")),
+ )),
+ tgrich.Ul(
+ tgrich.NewListItem(tgrich.P(tgrich.Text("бэкапы"))).
+ SetCheckbox().SetChecked().Build(),
+ tgrich.NewListItem(tgrich.P(tgrich.Text("миграция"))).
+ SetCheckbox().Build(),
),
)
return nil
}
```
-`RichAnswerKeyboard(kb, items...)` прикрепляет inline-клавиатуру.
+`RichAnswerKeyboard(keyboard, blocks...)` отправляет те же блоки с inline-клавиатурой.
-## DSL в `tgfmt`
+## Сборка текста и блоков
-Два типа фрагментов превращают невалидную вложенность в ошибку компиляции:
+Inline-конструкторы: `Text`, `Concat`, `Bold`, `Italic`, `Underline`, `Strikethrough`, `Spoiler`, `Code`, `Marked`, `Subscript`, `Superscript`, `URL`, `Email`, `Phone`, `TextMention`, `Mention`, `Hashtag`, `Cashtag`, `BotCommand`, `Emoji`, `DateTime`, `MathExpression`, anchors и references.
-- `tgfmt.Rich` — inline-содержимое (жирный, ссылки, эмодзи, время, математика, ...);
-- `tgfmt.RichBlock` — блочное содержимое (заголовки, абзацы, списки, таблицы, медиа, ...).
+Блочные конструкторы: `P`, `H1`-`H6`, `Pre`, `CodeBlock`, `Footer`, `Hr`, `Math`, `Anchor`, `Ul`, `Ol`, цитаты, коллажи, слайд-шоу, таблицы, details, карты, анимации, аудио, фото, видео, голосовые сообщения и доступный только в черновиках блок `Thinking`.
-Блочные конструкторы принимают только `Rich`-аргументы, поэтому таблица внутри абзаца не скомпилируется. На *верхнем уровне* допустимы оба (`RichItem`): соседний inline-контент Telegram сам собирает в абзацы.
+Для элементов списков и таблиц используются небольшие builders:
-Сырой текст попадает в DSL ровно одним способом — `tgfmt.NewRich("...")`, который экранирует HTML. Всё остальное — композиция уже безопасных фрагментов.
+```go
+ordered := tgrich.Ol(
+ tgrich.OlOpts{Start: 3, Type: tgapi.InputRichBlockListItemTypeLower},
+ tgrich.NewListItem(tgrich.P(tgrich.Text("третий"))).Build(),
+)
-Inline-хелперы (методы `Rich`): `Bold`, `Italic`, `Underline`, `Strike`, `Code`, `Mark`, `Sub`, `Sup`, `Spoiler`, `Link`, `Email`, `Phone`, `Mention`, `Anchor`, `AnchorLink`, `Ref`, `Time`, `TimeFormat`, `Math`; свободные функции `Emoji`, `Br`.
+table := tgrich.NewTable(
+ tgrich.Row(
+ tgrich.CellWithText(tgrich.Text("Имя")).SetHeader().Build(),
+ tgrich.CellWithText(tgrich.Text("Значение")).SetHeader().Build(),
+ ),
+ tgrich.Row(
+ tgrich.CellWithText(tgrich.Text("Статус")).Build(),
+ tgrich.CellWithText(tgrich.Bold(tgrich.Text("ОК"))).Build(),
+ ),
+).SetBordered(true).Build()
+```
-Блочные конструкторы: `H1`–`H6`, `P`, `Pre`, `PreCode`, `Footer`, `Hr`, `AnchorBlock`, `Ul`/`Ol` с `Li`/`LiCheckbox`, `Blockquote`, `Aside`, `Photo`/`Video`/`Audio` (медиа-билдеры с `.Block()`, `.Caption(...)`, `.SetSpoiler()`), `Map`/`MapCaption`, `Collage`/`Slideshow` (+варианты `...Caption`), `Table`/`Row`/`Cell`, `Details`, `MathBlock`.
+## Block payload и преобразование в HTML
-Чтобы собрать payload без отправки, используйте `tgfmt.RichHTML(items...)` (строка) или `tgfmt.RichMessage(items...)` (готовый `tgapi.InputRichMessage` с включённым `skip_entity_detection`, чтобы сервер не добавлял авто-entities).
+Если преобразование не требуется, отправляйте input-дерево напрямую:
-## Прямой доступ через `tgapi`
+```go
+rich := tgapi.InputRichMessage{
+ Blocks: []tgapi.InputRichBlock{
+ tgrich.H1(tgrich.Text("Отчёт")),
+ tgrich.P(tgrich.Text("Готово")),
+ },
+}
+```
-- `API.SendRichMessage(params)` — отправка; `params.RichMessage` — это `InputRichMessage`.
-- `API.SendRichMessageDraft(params)` — стриминг частичного сообщения во время генерации. Черновик — эфемерное превью на ~30 секунд с ключом `DraftID` (обновления с тем же ID анимируются); в конце вызовите `SendRichMessage` с полным сообщением. Черновики — единственное место, где встречается блок `thinking`.
-- `EditMessageText.RichMessage` — редактирование rich-сообщения на месте (`Text` оставьте пустым).
-- `InputRichMessageContent` — rich-контент для результатов inline-запросов (`input_message_content`).
+`tgrich.BuildHTML(blocks...)` валидирует целое дерево и преобразует его в HTML-вариант `InputRichMessage`. `tgrich.ToHTML(block)` — сокращённая форма для одного блока. Преобразование полезно для логов, превью и API вроде `MessageContext.RichAnswer`, которые отправляют отрендеренный HTML.
+
+`BuildHTML` включает `SkipEntityDetection`, экранирует текст и атрибуты и проверяет официальные лимиты Telegram:
+
+- 32768 UTF-8 символов;
+- 500 блоков, включая вложенные блоки, элементы списков и строки таблиц;
+- 16 общих уровней вложенных блоков и форматирования;
+- 50 media attachments;
+- 20 колонок таблицы с учётом `colspan`.
+
+Также проверяются discriminators блоков, размеры заголовков, типы маркеров и checkbox-состояние списков, выравнивание и spans таблиц, координаты/zoom/размеры карт и типы медиа.
+
+## Медиа и multipart-загрузки
+
+Медиа-конструкторы принимают `tgapi.InputMedia`, поэтому поле `Media` может содержать HTTP URL, Telegram `file_id` или ссылку `attach://name`:
+
+```go
+block := tgrich.PhotoWithCaption(
+ tgapi.InputMedia{Media: "telegram-file-id"},
+ tgrich.CaptionWithCredit(
+ tgrich.Text("Запуск"),
+ tgrich.Text("Эксплуатация"),
+ ),
+)
+```
+
+При преобразовании в HTML медиа-блоки превращаются в ссылки `tg://photo?id=...`, `tg://video?id=...` или `tg://audio?id=...`. Исходные значения `InputMedia` собираются в `InputRichMessage.Media` со стабильными идентификаторами `media_1`, `media_2`, ...
+
+Для multipart-загрузки имя после `attach://` должно совпадать с именем multipart-поля:
+
+```go
+rich, err := tgrich.BuildHTML(
+ tgrich.Photo(tgapi.InputMedia{Media: "attach://report"}),
+)
+if err != nil {
+ return err
+}
+
+_, err = uploader.SendRichMessage(
+ tgapi.SendRichMessage{ChatID: chatID, RichMessage: rich},
+ tgapi.NewUploaderFile("report.jpg", data).SetAttachName("report"),
+)
+```
+
+Та же схема загрузки доступна через `Uploader.SendRichMessageDraft`.
+
+## Прямой доступ через API
+
+- `API.SendRichMessage` отправляет завершённое rich-сообщение.
+- `API.SendRichMessageDraft` обновляет эфемерное превью во время генерации. `Thinking` допустим только в черновиках.
+- `Uploader.SendRichMessage` и `Uploader.SendRichMessageDraft` загружают файлы, указанные через `attach://`.
+- `EditMessageText.RichMessage` редактирует существующее rich-сообщение; оставьте `Text` пустым.
+- `InputRichMessageContent` задаёт rich-контент для inline-результатов.
## Приём
-`Message.RichMessage` (`*tgapi.RichMessage`) разбирается автоматически. Обходите его type switch-ами:
+`Message.RichMessage` разбирается автоматически. Обходите принятое дерево через type switch:
```go
for _, block := range msg.RichMessage.Blocks {
@@ -74,23 +144,25 @@ for _, block := range msg.RichMessage.Blocks {
handleText(b.Text)
case tgapi.RichBlockList:
for _, item := range b.Items {
- // item.Label — готовый видимый маркер: "1.", "c.", "vii.", "•"
+ // Label — готовый серверный маркер: "1.", "c.", "vii." или "•".
}
}
}
```
-Неизвестные будущие типы узлов с полем `text` сохраняются как `RichTextWrap`/`RichBlockWrap`, а не роняют разбор — парсинг переживает расширения API.
+Неизвестные будущие узлы с полем `text` сохраняются как `RichTextWrap` или `RichBlockWrap`, поэтому parser переносит совместимые расширения Bot API.
## Подводные камни
-- **Медиа — только по URL.** В HTML-режиме ``/`