diff --git a/Scenes-RU.md b/Scenes-RU.md index 76a808a..0d1dda7 100644 --- a/Scenes-RU.md +++ b/Scenes-RU.md @@ -13,17 +13,19 @@ DRAFT: эта страница описывает предлагаемую мо ## Цели дизайна -- Сцены должны быть опциональными и additve. +- Сцены должны быть опциональными и additive. - Plugins должны оставаться основной единицей регистрации. - `MsgContext` должен оставаться базовым контекстом для обычных handlers. - Нужно покрыть и пошаговые flow, и модальные chat loops. - Нужно явно различать сессии в личке и в чате. - Локальные stop/escape-команды сцены должны быть first-class механизмом. +- Нужен встроенный способ хранить состояние сцены между шагами. ## Ментальная модель - Команды и payload маршрутизируются по trigger. - Сцены маршрутизируются по активному состоянию. +- Сцены являются отдельным слоем маршрутизации поверх commands, payloads и обычных update handlers. - Команда вроде `/rpstart` входит в сцену. - Пока сцена активна, обычные сообщения сначала уходят в сцену. - Локальные команды сцены вроде `/rpstop` парсятся внутри активной сцены. @@ -59,6 +61,7 @@ const ( type SceneSession struct { Scene string Step string + Data []byte } type SessionStore interface { @@ -80,6 +83,8 @@ plugin.NewScene("rp"). AddCommand("rpstop", stopRP) ``` +`SetEntry(...)` задаёт начальный step или state сцены. Когда `ctx.EnterScene("rp")` создаёт новую сессию, фреймворк записывает это значение в `SceneSession.Step`. + Так plugin API остаётся визуально согласованным: - `NewCommand(...)` @@ -93,6 +98,7 @@ plugin.NewScene("rp"). - Пошаговые сцены: именованный step обрабатывает каждый update и выбирает следующий step. - Модальные сцены: долгоживущий "режим" обрабатывает обычные сообщения, пока пользователь явно не выйдет. +- Обе формы должны уметь регистрировать локальные команды сцены. Пример пошагового flow: @@ -172,6 +178,41 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { } ``` +## Состояние между шагами + +Для реальных сценариев недостаточно хранить только `Scene` и `Step`. Обычно между update нужно накапливать данные: черновик профиля, выбранные параметры, временные ID и другие промежуточные значения. + +Это не стоит полностью перекладывать на разработчика приложения. Фреймворк должен давать слот для хранения в `SceneSession` и удобные helper-методы на `SceneContext`. + +Предлагаемый helper API: + +- `ctx.BindState(&dst)` +- `ctx.SaveState(src)` +- `ctx.ClearState()` + +Пример: + +```go +type ProfileDraft struct { + Name string + Age int +} + +func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { + var draft ProfileDraft + _ = ctx.BindState(&draft) + + draft.Name = ctx.Text + if err := ctx.SaveState(draft); err != nil { + return nil, err + } + + return ctx.Next("age"), nil +} +``` + +Использование `Data []byte` в `SceneSession` делает контракт `SessionStore` маленьким и не завязанным на конкретный формат хранения. В первой реализации scene helpers могут работать через JSON. + ## Алгоритм маршрутизации Роутер должен вести себя так: @@ -183,7 +224,7 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { 5. Если сцена активна, сначала попробовать scene-local command или payload routing. 6. Если локальный route не совпал, вызвать message handler или step handler сцены. 7. Если сцена вернула `Stay`, сохранить текущую сессию без изменений. -8. Если сцена вернула `Next(step)`, сохранить новый step. +8. Если сцена вернула `Next(step)`, сохранить новый step и оставить `Data`. 9. Если сцена вернула `Exit`, удалить сессию. 10. Если сцена вернула `Pass`, продолжить обычную маршрутизацию. @@ -194,11 +235,11 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { - Middleware должны продолжать работать на базовом `MsgContext`. - Scene handlers должны так же пользоваться `ctx.BindArgs(...)` и `ctx.Context()`. - Первая версия может поставляться с in-memory store плюс интерфейсом `SessionStore` для кастомного persistence. +- Helpers для сериализации состояния должны входить в первый scene API, а не оставаться приложенческим boilerplate. ## Открытые вопросы - Нужно ли добавлять scene-local payloads уже в первой версии или отложить на вторую? -- Должен ли `SceneContext` напрямую предоставлять helpers для scene-session storage? - Стоит ли поставлять `MemorySessionStore` по умолчанию, или сцены должны требовать явный store? - Должны ли modal scenes иметь явный fallback mode вроде "pass to normal routing" против "consume silently"? @@ -210,6 +251,7 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { - Добавить `MsgContext.EnterScene(...)`, `ExitScene(...)` и `CurrentScene(...)`. - Маршрутизировать активные сцены раньше обычного command/payload flow. - Поддержать scene-local commands и `OnMessage(...)`. +- Поддержать helpers для состояния сцены поверх `SceneSession.Data`. Связанные страницы: diff --git a/Scenes.md b/Scenes.md index 19cbddf..a0701df 100644 --- a/Scenes.md +++ b/Scenes.md @@ -19,11 +19,13 @@ Scenes are the planned stateful routing layer for long-lived interactions in Lan - Support both step-based flows and modal chat loops. - Distinguish private-chat and group-chat sessions explicitly. - Make scene-local stop/escape commands first-class. +- Provide a built-in way to persist scene state between steps. ## Mental model - Commands and payloads route by trigger. - Scenes route by active state. +- Scenes are a separate routing layer above commands, payloads, and generic update handlers. - A command such as `/rpstart` enters a scene. - Once the scene is active, regular messages are routed to the scene first. - Scene-local commands such as `/rpstop` are parsed inside the active scene. @@ -59,6 +61,7 @@ const ( type SceneSession struct { Scene string Step string + Data []byte } type SessionStore interface { @@ -80,6 +83,8 @@ plugin.NewScene("rp"). AddCommand("rpstop", stopRP) ``` +`SetEntry(...)` defines the initial step or state of the scene. When `ctx.EnterScene("rp")` creates a new session, the framework writes that entry step into `SceneSession.Step`. + This keeps the plugin API visually consistent: - `NewCommand(...)` @@ -93,6 +98,7 @@ The first version should support two common shapes. - Step-based scenes: a named step handles each update and decides the next step. - Modal scenes: a long-lived "mode" handles ordinary messages until an explicit exit command ends it. +- Both shapes should still be able to register scene-local commands. Step-based flow example: @@ -172,6 +178,41 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { } ``` +## State between steps + +Scenes need more than just `Scene` and `Step`. Real flows also accumulate data between updates, such as a draft profile, selected options, or temporary IDs. + +That state should not be pushed entirely onto the application author. The framework should provide a storage slot inside `SceneSession` plus ergonomic helpers on `SceneContext`. + +Proposed helper API: + +- `ctx.BindState(&dst)` +- `ctx.SaveState(src)` +- `ctx.ClearState()` + +Example: + +```go +type ProfileDraft struct { + Name string + Age int +} + +func askName(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) { + var draft ProfileDraft + _ = ctx.BindState(&draft) + + draft.Name = ctx.Text + if err := ctx.SaveState(draft); err != nil { + return nil, err + } + + return ctx.Next("age"), nil +} +``` + +Using `Data []byte` in `SceneSession` keeps the `SessionStore` contract small and storage-agnostic. The scene helpers can use JSON for the first implementation. + ## Routing algorithm The router should behave like this: @@ -183,7 +224,7 @@ The router should behave like this: 5. If a scene is active, try scene-local command or payload routing first. 6. If no scene-local route matches, try the scene message or step handler. 7. If the scene returns `Stay`, keep the current session. -8. If the scene returns `Next(step)`, persist the new step. +8. If the scene returns `Next(step)`, persist the new step and keep `Data`. 9. If the scene returns `Exit`, delete the session. 10. If the scene returns `Pass`, continue normal routing. @@ -194,11 +235,11 @@ The router should behave like this: - Middleware should still run on the base `MsgContext`. - Scene handlers should continue to benefit from `ctx.BindArgs(...)` and `ctx.Context()`. - The first version can ship with an in-memory store plus a `SessionStore` interface for custom persistence. +- State serialization helpers should be part of the first scene API, not left as app-specific boilerplate. ## Open questions - Should scene-local payloads be added in the first version or the second? -- Should `SceneContext` expose scene-session storage helpers directly? - Should the framework ship with a default `MemorySessionStore`, or should scenes require an explicit store? - Should modal scenes have an explicit fallback mode such as "pass to normal routing" versus "consume silently"? @@ -210,6 +251,7 @@ The router should behave like this: - Add `MsgContext.EnterScene(...)`, `ExitScene(...)`, and `CurrentScene(...)`. - Route active scenes before normal command and payload flow. - Support scene-local commands and `OnMessage(...)`. +- Support scene-state helpers backed by `SceneSession.Data`. Related pages: diff --git a/_Sidebar.md b/_Sidebar.md index 1037880..21f0ddb 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -12,6 +12,7 @@ - [[Bot-Lifecycle]] - [[Middleware]] - [[Scenes]] +- [[Scenes-RU|Scenes (RU)]] ## Changes and troubleshooting - [[Migration]] @@ -26,6 +27,8 @@ - [[Semver-and-Releases]] - [[Page-Priority]] -## Drafts and RU companion pages -- [[Scenes-RU]] -- [[Framework-Backlog-RU]] +## Drafts +- [[Framework-Backlog]] +- [[Scenes]] +- [[Framework-Backlog-RU|Framework Backlog (RU)]] +- [[Scenes-RU|Scenes (RU)]]