mcpField
T
telegram-bot-sessions

telegram-bot-sessions

github.com/agntdev开发143 次安装17 周前更新
代码仓库

关于 telegram-bot-sessions

telegram-bot-sessions 是面向 Telegram bot 开发者的技能,介绍如何用 grammY 管理会话状态。内容涵盖内存存储、SQLite 持久化方案,以及测试间的状态隔离。

主要功能

  1. 01说明 grammY 会话与 ctx.session
  2. 02介绍 MemorySessionStorage
  3. 03讲解 SQLite 存储与适配器
  4. 04提供会话结构设计建议
  5. 05说明测试用例的状态隔离

适用场景

  1. 01实现跨消息的对话流程
  2. 02选择开发或生产会话存储
  3. 03设计会话数据与字段变更
  4. 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。机器人需要自行:

  1. 记住每个人当前进行到哪一步
  2. 在不同消息之间存储部分数据
  3. 在流程完成时清除状态

方案:数据库。 每收到一条消息,就将用户状态写入 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-sqliteSqliteSessionStorage(计划中)
会话键chatId_userId相同
测试框架隔离手动设置自动——每个 spec 都是全新的

常见错误

  1. 存储不可序列化的数据 — 不要存储函数或类实例,只存储普通对象。
  2. 未在 initial() 中初始化字段 — 缺失的字段值为 undefined,而不是默认值。
  3. 依赖重启后仍保留的会话(MemoryStorage) — 会话是临时的。设计可安全重启的流程。
  4. 会话按聊天划分,而非按用户划分 — 同一用户在不同聊天中的会话各不相同。
  5. 会话键 = chatId_userId 字符串 — 不要与单独的 chat.id 混淆。

评论

支持 Markdown