关于 telegram-bot-sessions
telegram-bot-sessions 是面向 Telegram bot 开发者的技能,介绍如何用 grammY 管理会话状态。内容涵盖内存存储、SQLite 持久化方案,以及测试间的状态隔离。
主要功能
- 01说明 grammY 会话与 ctx.session
- 02介绍 MemorySessionStorage
- 03讲解 SQLite 存储与适配器
- 04提供会话结构设计建议
- 05说明测试用例的状态隔离
适用场景
- 01实现跨消息的对话流程
- 02选择开发或生产会话存储
- 03设计会话数据与字段变更
- 04为每个测试用例隔离 bot 状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agntdev/ skills --skill telegram-bot-sessions安装后在 Claude Code 中输入/telegram-bot-sessions确认已加载。
详细介绍
telegram-bot-sessions Skill
如何在 Telegram 机器人中持久化用户状态——为什么需要这样做、grammY 如何实现,以及工具包如何封装。
为 agntdev 流水线构建。 请参阅 agnt-cli-builder,了解发现与认领 循环。本 Skill 介绍 MemorySessionStorage / SQLite 适配器模式,你可以将其接入已认领任务的实现中。
1. 为什么需要会话(Bot API)
Telegram Bot API 是无状态的。每个 Update 都是一个全新的 HTTP 请求。机器人没有内置记忆,无法知道用户之前做过什么。
User: "I want to book"
Bot: "What service?"
User: "Haircut"
Bot: "When?"
User: "Tomorrow 2pm"
Bot: "Booked!"
没有状态,机器人就无法判断“Haircut”是在回答“什么服务?”这个问题,还是一条随意发送的消息。它不记得当前对话进行到了哪一步。
Bot API 如何处理——它没有内置机制
Bot API 只负责传递 Update。机器人需要自行:
- 记住每个人当前进行到哪一步
- 在不同消息之间存储部分数据
- 在流程完成时清除状态
方案:数据库。 每收到一条消息,就将用户状态写入 SQLite/Redis。可行,但会增加延迟和复杂度。
方案:内存 Map。 速度快,但重启后数据会丢失。适合开发,不适合生产环境。
方案:grammY 会话。 框架会帮你处理这些细节。
2. grammY 会话——框架解决方案
grammY 提供了一个 session() 插件,用于存储每个聊天的状态,并通过 ctx.session 提供该状态。
基本设置
import { Bot, session } from "grammy";
interface SessionData {
step: string;
service?: string;
}
const bot = new Bot(token);
bot.use(session({
initial: (): SessionData => ({ step: "idle" }),
// storage: ... // defaults to in-memory Map
}));
bot.command("book", async (ctx) => {
ctx.session.step = "choosing_service";
await ctx.reply("What service?");
});
bot.on("message:text", async (ctx) => {
if (ctx.session.step === "choosing_service") {
ctx.session.service = ctx.message.text;
ctx.session.step = "choosing_time";
await ctx.reply("What time?");
}
});
会话键
grammY 使用 chatId_userId("12345_67890")作为会话键。这意味着:
- 私聊: 每位用户有一个会话(聊天对象和用户是同一个人)
- 群聊: 聊天中的所有用户共享一个会话
- 会话按聊天区分,而非全局按用户区分
会话类型——为 ctx.session 指定类型
import { session, type SessionFlavor } from "grammy";
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>(token);
bot.use(session({ initial: () => ({ step: "idle" }) }));
// ctx.session is now typed:
bot.command("book", async (ctx) => {
ctx.session.step = "choosing"; // TypeScript knows this field
});
StorageAdapter 接口
grammY 会话可使用任何实现以下接口的存储后端:
interface StorageAdapter<T> {
read(key: string): T | undefined;
write(key: string, value: T): void;
delete(key: string): void;
has(key: string): boolean;
readAllKeys(): string[];
}
内置选项:
- 默认: 内存
Map(速度快,重启后数据会丢失,适合开发) - SQLite: 持久化,重启后仍可保留(添加
@grammyjs/storage-sqlite) - Redis: 速度快且持久化(添加
@grammyjs/storage-redis) - Firebase、MongoDB、Supabase: 可使用社区适配器
3. 工具包(src/toolkit/)——会话默认配置
MemorySessionStorage
bot-starter 模板中的内置工具包(位于 src/toolkit/)
提供了 MemorySessionStorage——一个由 Map 支持、与 grammY 兼容的 StorageAdapter。
import { MemorySessionStorage } from "../src/toolkit/storage/memory.js";
// Implements StorageAdapter:
const store = new MemorySessionStorage<SessionData>();
store.write("123_456", { step: "idle" });
store.read("123_456"); // { step: "idle" }
store.has("123_456"); // true
store.delete("123_456");
store.readAllKeys(); // []
通常无需直接实例化它。 createBot() 默认会使用它:
const bot = createBot<Session>(token, {
initial: () => ({ step: "idle" }),
// storage omitted → MemorySessionStorage used automatically
});
会话结构设计
会话应保持扁平且可序列化——不包含函数、类实例或循环引用:
interface Session {
// Dialog state
step: string;
// Flow data (optional until set)
serviceId?: string;
slotDate?: string;
slotTime?: string;
// Simple counters
bookingsCount?: number;
}
每个会话都从 initial() 开始:
initial: () => ({
step: "idle",
bookingsCount: 0,
})
测试框架隔离
测试框架会通过 makeBot() 为每个 spec 创建一个全新的机器人。每个机器人都有自己的 MemorySessionStorage:
Spec 1 ("booking flow"):
makeBot() → fresh MemorySessionStorage → session starts from initial()
Spec 2 ("cancel flow"):
makeBot() → another fresh MemorySessionStorage → session starts from initial()
- spec 之间不会泄漏会话
- 各次运行之间无需清理
- 每个 spec 看到的状态都严格由
initial()定义
生产环境中的 SQLite
对于生产环境中的机器人,可切换到 SQLite(使用相同的 StorageAdapter 接口,机器人代码也无需更改):
import { SqliteSessionStorage } from "../src/toolkit/storage/sqlite.js"; // planned
const bot = createBot<Session>(token, {
initial: () => ({ step: "idle" }),
storage: new SqliteSessionStorage("./data/sessions.db"),
});
在工具包提供 SQLite 适配器之前,请直接使用 grammY 的 @grammyjs/storage-sqlite——接口相同。
迁移
向会话添加字段:
// V1
interface Session { step: string; }
// V2 — add optional field (safe, no migration needed)
interface Session {
step: string;
theme?: "light" | "dark"; // optional — defaults to undefined
}
// Access with default:
const theme = ctx.session.theme ?? "light";
- MemoryStorage: 重启会清除所有内容——无需迁移
- SQLite: 添加可选字段是安全的。新增的必填字段需要迁移步骤,为现有行填充默认值
快速参考
| 内容 | grammY | 工具包 |
|---|---|---|
| 启用会话 | bot.use(session({...})) | 通过 createBot() 自动启用 |
| 为 ctx.session 指定类型 | Context & SessionFlavor<S> | BotContext<S>(预先构建) |
| 存储(开发) | 内存 Map(默认) | MemorySessionStorage(默认) |
| 存储(生产) | @grammyjs/storage-sqlite | SqliteSessionStorage(计划中) |
| 会话键 | chatId_userId | 相同 |
| 测试框架隔离 | 手动设置 | 自动——每个 spec 都是全新的 |
常见错误
- 存储不可序列化的数据 — 不要存储函数或类实例,只存储普通对象。
- 未在
initial()中初始化字段 — 缺失的字段值为undefined,而不是默认值。 - 依赖重启后仍保留的会话(MemoryStorage) — 会话是临时的。设计可安全重启的流程。
- 会话按聊天划分,而非按用户划分 — 同一用户在不同聊天中的会话各不相同。
- 会话键 =
chatId_userId字符串 — 不要与单独的chat.id混淆。