第三步:状态、会话与中间件

一个 Runtime 开始服务真实用户后,很快会遇到三类需求:记住某个插件的数据、等待用户的下一条消息、 以及在多个 handler 前后执行同一段逻辑。这一章分别对应状态、会话和中间件。

插件状态

插件的 ctx.state 默认是插件私有的。把 state_scope 声明为 persistent 后, 它会配合 [state] 后端跨重启保存。

from iamai import Context, Plugin, command


class CounterPlugin(Plugin):
    name = "counter"
    state_scope = "persistent"

    @command("count")
    async def count(self, ctx: Context) -> None:
        value = int(ctx.state.get("value", 0)) + 1
        ctx.state["value"] = value
        await ctx.reply(f"count={value}")

对应配置可以先用 JSON,部署后再按需要切到 SQLite:

[state]
backend = "json"
path = "var/state.json"

多轮会话

wait_for_message 会等待同一个会话键下的下一条消息。它适合短流程,例如确认、问卷、 补充参数,不适合长时间业务状态机。

from iamai import Context, Plugin, command


class SurveyPlugin(Plugin):
    name = "survey"

    @command("survey")
    async def survey(self, ctx: Context) -> None:
        await ctx.reply("你喜欢的语言是?")
        answer = await ctx.wait_for_message(timeout=30)
        await ctx.reply(f"收到:{answer.text}")

如果超时,handler 会抛出超时异常;生产插件通常应捕获它并给用户一个明确反馈。

中间件

中间件适合横切逻辑,例如统一审计、输入规范化、耗时统计或异常兜底。不要把核心业务流程藏进 中间件里,否则 handler 的行为会变得难以阅读。

from iamai import Context, Plugin, middleware


class AuditPlugin(Plugin):
    name = "audit"

    @middleware("before")
    async def before(self, ctx: Context) -> None:
        ctx.runtime.count_metric("handler_input_seen", adapter=ctx.event.adapter)

中间件阶段包括 beforearoundaftererror。当你只需要一个 handler 自己的前置逻辑时,直接写在 handler 里通常更清楚。

Checkpoint

到这里你应该能区分:

  • ctx.state 用于插件自己的数据;

  • ctx.shared_state 用于确有必要跨插件共享的数据;

  • wait_for_message 用于短时间多轮交互;

  • middleware 用于横切能力,而不是业务主流程。