REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY

Refresh scenes draft wiki

Clarify scene routing, entry steps, and local commands

Document scene state storage and expose RU draft links in sidebar
2026-03-27 13:01:31 +03:00
parent 47c98ed7bf
commit 5408d818f7
3 changed files with 95 additions and 8 deletions
+45 -3
@@ -13,17 +13,19 @@ DRAFT: эта страница описывает предлагаемую мо
## Цели дизайна ## Цели дизайна
- Сцены должны быть опциональными и additve. - Сцены должны быть опциональными и additive.
- Plugins должны оставаться основной единицей регистрации. - Plugins должны оставаться основной единицей регистрации.
- `MsgContext` должен оставаться базовым контекстом для обычных handlers. - `MsgContext` должен оставаться базовым контекстом для обычных handlers.
- Нужно покрыть и пошаговые flow, и модальные chat loops. - Нужно покрыть и пошаговые flow, и модальные chat loops.
- Нужно явно различать сессии в личке и в чате. - Нужно явно различать сессии в личке и в чате.
- Локальные stop/escape-команды сцены должны быть first-class механизмом. - Локальные stop/escape-команды сцены должны быть first-class механизмом.
- Нужен встроенный способ хранить состояние сцены между шагами.
## Ментальная модель ## Ментальная модель
- Команды и payload маршрутизируются по trigger. - Команды и payload маршрутизируются по trigger.
- Сцены маршрутизируются по активному состоянию. - Сцены маршрутизируются по активному состоянию.
- Сцены являются отдельным слоем маршрутизации поверх commands, payloads и обычных update handlers.
- Команда вроде `/rpstart` входит в сцену. - Команда вроде `/rpstart` входит в сцену.
- Пока сцена активна, обычные сообщения сначала уходят в сцену. - Пока сцена активна, обычные сообщения сначала уходят в сцену.
- Локальные команды сцены вроде `/rpstop` парсятся внутри активной сцены. - Локальные команды сцены вроде `/rpstop` парсятся внутри активной сцены.
@@ -59,6 +61,7 @@ const (
type SceneSession struct { type SceneSession struct {
Scene string Scene string
Step string Step string
Data []byte
} }
type SessionStore interface { type SessionStore interface {
@@ -80,6 +83,8 @@ plugin.NewScene("rp").
AddCommand("rpstop", stopRP) AddCommand("rpstop", stopRP)
``` ```
`SetEntry(...)` задаёт начальный step или state сцены. Когда `ctx.EnterScene("rp")` создаёт новую сессию, фреймворк записывает это значение в `SceneSession.Step`.
Так plugin API остаётся визуально согласованным: Так plugin API остаётся визуально согласованным:
- `NewCommand(...)` - `NewCommand(...)`
@@ -93,6 +98,7 @@ plugin.NewScene("rp").
- Пошаговые сцены: именованный step обрабатывает каждый update и выбирает следующий step. - Пошаговые сцены: именованный step обрабатывает каждый update и выбирает следующий step.
- Модальные сцены: долгоживущий "режим" обрабатывает обычные сообщения, пока пользователь явно не выйдет. - Модальные сцены: долгоживущий "режим" обрабатывает обычные сообщения, пока пользователь явно не выйдет.
- Обе формы должны уметь регистрировать локальные команды сцены.
Пример пошагового flow: Пример пошагового 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. 5. Если сцена активна, сначала попробовать scene-local command или payload routing.
6. Если локальный route не совпал, вызвать message handler или step handler сцены. 6. Если локальный route не совпал, вызвать message handler или step handler сцены.
7. Если сцена вернула `Stay`, сохранить текущую сессию без изменений. 7. Если сцена вернула `Stay`, сохранить текущую сессию без изменений.
8. Если сцена вернула `Next(step)`, сохранить новый step. 8. Если сцена вернула `Next(step)`, сохранить новый step и оставить `Data`.
9. Если сцена вернула `Exit`, удалить сессию. 9. Если сцена вернула `Exit`, удалить сессию.
10. Если сцена вернула `Pass`, продолжить обычную маршрутизацию. 10. Если сцена вернула `Pass`, продолжить обычную маршрутизацию.
@@ -194,11 +235,11 @@ func stopRP(ctx *laniakea.SceneContext, db *App) (laniakea.SceneResult, error) {
- Middleware должны продолжать работать на базовом `MsgContext`. - Middleware должны продолжать работать на базовом `MsgContext`.
- Scene handlers должны так же пользоваться `ctx.BindArgs(...)` и `ctx.Context()`. - Scene handlers должны так же пользоваться `ctx.BindArgs(...)` и `ctx.Context()`.
- Первая версия может поставляться с in-memory store плюс интерфейсом `SessionStore` для кастомного persistence. - Первая версия может поставляться с in-memory store плюс интерфейсом `SessionStore` для кастомного persistence.
- Helpers для сериализации состояния должны входить в первый scene API, а не оставаться приложенческим boilerplate.
## Открытые вопросы ## Открытые вопросы
- Нужно ли добавлять scene-local payloads уже в первой версии или отложить на вторую? - Нужно ли добавлять scene-local payloads уже в первой версии или отложить на вторую?
- Должен ли `SceneContext` напрямую предоставлять helpers для scene-session storage?
- Стоит ли поставлять `MemorySessionStore` по умолчанию, или сцены должны требовать явный store? - Стоит ли поставлять `MemorySessionStore` по умолчанию, или сцены должны требовать явный store?
- Должны ли modal scenes иметь явный fallback mode вроде "pass to normal routing" против "consume silently"? - Должны ли 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(...)`. - Добавить `MsgContext.EnterScene(...)`, `ExitScene(...)` и `CurrentScene(...)`.
- Маршрутизировать активные сцены раньше обычного command/payload flow. - Маршрутизировать активные сцены раньше обычного command/payload flow.
- Поддержать scene-local commands и `OnMessage(...)`. - Поддержать scene-local commands и `OnMessage(...)`.
- Поддержать helpers для состояния сцены поверх `SceneSession.Data`.
Связанные страницы: Связанные страницы:
+44 -2
@@ -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. - Support both step-based flows and modal chat loops.
- Distinguish private-chat and group-chat sessions explicitly. - Distinguish private-chat and group-chat sessions explicitly.
- Make scene-local stop/escape commands first-class. - Make scene-local stop/escape commands first-class.
- Provide a built-in way to persist scene state between steps.
## Mental model ## Mental model
- Commands and payloads route by trigger. - Commands and payloads route by trigger.
- Scenes route by active state. - 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. - A command such as `/rpstart` enters a scene.
- Once the scene is active, regular messages are routed to the scene first. - 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. - Scene-local commands such as `/rpstop` are parsed inside the active scene.
@@ -59,6 +61,7 @@ const (
type SceneSession struct { type SceneSession struct {
Scene string Scene string
Step string Step string
Data []byte
} }
type SessionStore interface { type SessionStore interface {
@@ -80,6 +83,8 @@ plugin.NewScene("rp").
AddCommand("rpstop", stopRP) 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: This keeps the plugin API visually consistent:
- `NewCommand(...)` - `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. - 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. - 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: 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 ## Routing algorithm
The router should behave like this: 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. 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. 6. If no scene-local route matches, try the scene message or step handler.
7. If the scene returns `Stay`, keep the current session. 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. 9. If the scene returns `Exit`, delete the session.
10. If the scene returns `Pass`, continue normal routing. 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`. - Middleware should still run on the base `MsgContext`.
- Scene handlers should continue to benefit from `ctx.BindArgs(...)` and `ctx.Context()`. - 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. - 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 ## Open questions
- Should scene-local payloads be added in the first version or the second? - 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 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"? - 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(...)`. - Add `MsgContext.EnterScene(...)`, `ExitScene(...)`, and `CurrentScene(...)`.
- Route active scenes before normal command and payload flow. - Route active scenes before normal command and payload flow.
- Support scene-local commands and `OnMessage(...)`. - Support scene-local commands and `OnMessage(...)`.
- Support scene-state helpers backed by `SceneSession.Data`.
Related pages: Related pages:
+6 -3
@@ -12,6 +12,7 @@
- [[Bot-Lifecycle]] - [[Bot-Lifecycle]]
- [[Middleware]] - [[Middleware]]
- [[Scenes]] - [[Scenes]]
- [[Scenes-RU|Scenes (RU)]]
## Changes and troubleshooting ## Changes and troubleshooting
- [[Migration]] - [[Migration]]
@@ -26,6 +27,8 @@
- [[Semver-and-Releases]] - [[Semver-and-Releases]]
- [[Page-Priority]] - [[Page-Priority]]
## Drafts and RU companion pages ## Drafts
- [[Scenes-RU]] - [[Framework-Backlog]]
- [[Framework-Backlog-RU]] - [[Scenes]]
- [[Framework-Backlog-RU|Framework Backlog (RU)]]
- [[Scenes-RU|Scenes (RU)]]