mcpField
T
telegram-bot-ui

telegram-bot-ui

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

关于 telegram-bot-ui

telegram-bot-ui 是用于构建 Telegram 机器人键盘及处理交互的技能,说明按钮、命令和 ForceReply 的适用场景,并提供菜单、确认和分页构建器。

主要功能

  1. 01讲解内联键盘和回复键盘
  2. 02选择按钮或文本输入方式
  3. 03构建菜单和确认键盘
  4. 04创建分页键盘控件
  5. 05支持 URL、复制文本和 Web App 按钮
  6. 06介绍 grammY 回调处理

适用场景

  1. 01添加按钮式主菜单
  2. 02使用 ForceReply 收集自由文本
  3. 03构建确认和分页流程
  4. 04添加可复制文本或 Mini App 按钮

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agntdev/skills --skill telegram-bot-ui

安装后在 Claude Code 中输入/telegram-bot-ui确认已加载。

附带文件references/grammY-keyboards.md

详细介绍

telegram-bot-ui Skill

如何连接 Telegram 键盘 — 纯机制说明。有关机器人应该说什么、给人什么感觉,请参阅 telegram-bot-ux。

⭐ 按钮优先 — 默认的功能入口方式

开发并发布这些机器人的负责人是非技术人员,用户也是如此。人们通过点击按钮来操作机器人,而不是靠记忆斜杠命令。因此,添加功能时,应当在 /start 主菜单中添加按钮,而不是新建一个 bot.command(...)。

在 bot-starter 模板中只需一行 — 在 src/handlers/<slug>.ts 顶部调用 registerMainMenuItem({ label, data }),并用 .callbackQuery(...) 处理其 data; 已提供的 /start 会呈现汇总后的菜单。机器人应该提供的斜杠命令只有 /start、/help,以及下面偶尔用到的自由格式文本输入。不要为每项功能都添加斜杠命令 — 否则机器人最终会有 20 多个晦涩、重叠的命令,没有人用得了。

这并不意味着“永远不用命令”:请参阅 按钮与命令的选择启发式 — 对于自由格式文本输入(搜索词、备注、地址、日期、时间、金额),命令和 ForceReply 仍然是合适的工具。按钮用于导航和选择;命令用于机器人无法预测的输入内容。

为 agntdev 流水线打造。 有关发现与认领循环,请参阅 agnt-cli-builder。本技能介绍内联按钮、菜单、 分页、confirmKeyboard、ForceReply 和 copy_text 模式,供你在已认领任务的实现中使用。


1. Telegram 键盘的工作原理(Bot API)

Telegram 有两种键盘类型,以及几个用于特殊输入的标记变体。用途不同,JSON 结构也不同。

InlineKeyboardMarkup

附加到特定消息的按钮。点击后会将 callback_query 发回机器人(不会发送消息到聊天)。适用于菜单、 确认和分页。

// Attached to sendMessage reply_markup field
{
  "inline_keyboard": [
    [
      { "text": "Yes", "callback_data": "confirm:42:yes" },
      { "text": "No",  "callback_data": "confirm:42:no" }
    ],
    [
      { "text": "Open Site", "url": "https://example.com" }
    ]
  ]
}

点击“Yes” → 机器人收到带有 data: "confirm:42:yes" 的 callback_query。

限制(参见 telegram-bot-api-fundamentals §4):

  • callback_data ≤ 64 字节(UTF-8)。西里尔字母 / emoji = 多字节。
  • 每个键盘 ≤ 100 个按钮。
  • ≤ 8 行(iOS 大约滚动到 5 行时)。
  • 按钮文本 ≤ 约 64 个字符(超出会被截断;移动设备上保持 ≤24 个字符)。
ReplyKeyboardMarkup

持久显示的按钮,会替换用户的键盘。点击后会发送一条普通文本消息。适用于持久菜单和快捷回复。

{
  "keyboard": [
    [{ "text": "📅 Book" }, { "text": "📋 My bookings" }],
    [{ "text": "❌ Cancel" }]
  ],
  "resize_keyboard": true,
  "one_time_keyboard": false,
  "input_field_placeholder": "Type or tap…",
  "selective": false
}

有用的参数:

  • resize_keyboard — 缩小按钮。Telegram Desktop 5.3.2+ 会忽略此参数(始终使用每个按钮 54 px 的扁平网格;水平宽度上限为 530 px)。
  • one_time_keyboard: true — 首次点击后隐藏键盘。适用于一次性提示。
  • input_field_placeholder — 键盘上方输入框中的文本。请使用此参数;用户能看到这条提示。
  • selective: true — 仅向群组中的特定用户显示此键盘(较少使用)。
ForceReply

要求用户在聊天中回复(界面会显示内联的“回复对象”提示)。 用于必须输入文本且机器人无法通过按钮获取的内容。

{
  "force_reply": true,
  "input_field_placeholder": "Send your booking address…",
  "selective": false
}

用户会在输入框上方看到“正在回复你的机器人…”标签。机器人必须显式处理下一条文本消息(按会话步骤筛选 — 参见 telegram-bot-ux §6 线性向导)。

按钮与命令——判断方法
用户需要……使用
从少量固定选项中选择(是/否、选项 A/B/C、页码 N)行内按钮
打开菜单、确认操作、翻页行内按钮
发送自由文本(合约地址、备注、搜索查询)/command + 文本输入
输入由机器人解析的结构化参数(日期、时间、金额)/command + 文本输入
必须通过文本回复(回答机器人的问题)ForceReply
用户想要反复点击的常驻快捷方式回复键盘(谨慎使用)

过去“默认使用行内按钮”的规则是错误的。按钮适用于选择;命令适用于自由输入。把每种输入都做成按钮的机器人会让高级用户反感。用命令来呈现菜单的机器人会让移动端用户反感。

拿不准时,问问自己:“用户知道要输入什么吗?”如果知道,就用命令。如果不知道(或者有效选项太多,难以记住),就用按钮。

关于按钮上的文案(动词优先、句式大小写、表情符号用量),请参阅 telegram-bot-ux §1 微文案。

编辑与发送
# Edit existing message (for inline keyboards — user doesn't see new messages)
POST /bot<TOKEN>/editMessageText   { chat_id, message_id, text, reply_markup }

# Edit only the keyboard (keep text)
POST /bot<TOKEN>/editMessageReplyMarkup  { chat_id, message_id, reply_markup }

# Send new message (for reply keyboard flows)
POST /bot<TOKEN>/sendMessage  { chat_id, text, reply_markup }

2. grammY——使用键盘

grammY 中附加键盘、处理回调,以及“编辑消息还是发送新消息”的 UX 模式,都在 references/grammY-keyboards.md 中介绍。需要查看行内键盘、回复键盘、ForceReply、请求联系人、请求位置、请求用户、请求聊天、请求受管理机器人的确切 reply_markup 结构、回调数据前缀路由规则,以及 Telegram 关于原位编辑的 UX 说明时,请阅读该文件。


3. 工具包(src/toolkit/)——UI 构建器

内联工具包(位于 bot-starter 模板的 src/toolkit/ 中)提供纯构建器,返回普通的 InlineKeyboardMarkup 对象。无需导入 grammY——它们会生成 grammY 所需的确切 JSON 结构。

import {
  inlineButton, urlButton, copyTextButton, webAppButton,
  inlineKeyboard, menuKeyboard, confirmKeyboard, paginate,
} from "../src/toolkit/ui/buttons.js";
inlineButton(text, callbackData)
inlineButton("Yes", "confirm:42")
// → { text: "Yes", callback_data: "confirm:42" }
// Type: { text: string; callback_data: string }
urlButton(text, url)
urlButton("Docs", "https://agnt-gm.ai")
// → { text: "Docs", url: "https://agnt-gm.ai" }
// Type: { text: string; url: string }
copyTextButton(text, copyText)——一键复制
copyTextButton("📋 Copy order ID", "ORD-12345")
// → { text: "📋 Copy order ID", copy_text: { text: "ORD-12345" } }
// Type: { text: string; copy_text: { text: string } }

可用于 ID、地址、代码、链接等任何用户原本需要长按才能复制的内容。每次交互可少点 4 次。请参阅 telegram-bot-ux §10 反模式“ID 不使用 copy_text”。

webAppButton(text, url)——打开 Mini App
webAppButton("🛒 Open shop", "https://shop.example.com/twa")
// → { text: "🛒 Open shop", web_app: { url: "https://shop.example.com/twa" } }
// Type: { text: string; web_app: { url: string } }

适用于流程已超出行内键盘承载能力的情况——请参阅 telegram-bot-ux §8 Mini App 升级的 4 个明确门槛。

inlineKeyboard(rows)

将按钮行包装为有效的 InlineKeyboardMarkup:

const kb = inlineKeyboard([
  [inlineButton("A", "a"), inlineButton("B", "b")],
  [urlButton("Docs", "https://x.io")],
]);

await ctx.reply("Choose:", { reply_markup: kb });
menuKeyboard(items, columns?)

根据扁平列表创建网格布局。默认 1 列。

const items = [
  { text: "📅 Book", data: "menu:book" },
  { text: "📋 My bookings", data: "menu:my" },
  { text: "❌ Cancel", data: "menu:cancel" },
  { text: "ℹ️ Help", data: "menu:help" },
];

menuKeyboard(items);     // vertical list, 1 per row
menuKeyboard(items, 2);  // 2-column grid
confirmKeyboard(actionPrefix, opts?)

是/否选项行。回调:<prefix>:yes / <prefix>:no。

confirmKeyboard("delete:42");
// → [✅ Yes ("delete:42:yes")] [❌ No ("delete:42:no")]

confirmKeyboard("publish", { yes: "🚀 Publish", no: "🔙 Back" });

处理程序:

bot.on("callback_query:data", async (ctx) => {
  const data = ctx.callbackQuery.data;
  if (data.startsWith("delete:")) {
    const [, id, action] = data.split(":");
    await ctx.editMessageText(action === "yes" ? `Deleted ${id}` : "Cancelled");
  }
  await ctx.answerCallbackQuery();
});
paginate(items, options)

将项目分页,并添加上一页/下一页控件。

const result = paginate(allItems, {
  page: 0,               // 0-based, auto-clamped
  perPage: 5,
  callbackPrefix: "pg",   // default: "page"
  prevLabel: "« Prev",
  nextLabel: "Next »",
});

// result.pageItems  — items for current page
// result.totalPages — how many pages
// result.page       — actual page number (may be clamped)
// result.controls   — InlineKeyboardMarkup with prev/next buttons

单页 → 空控件。第一页 → 仅显示下一页。最后一页 → 仅显示上一页。中间页 → 两者都显示。

完整的分页处理程序:

async function showPage(ctx: BotContext, page: number) {
  const items = await loadItems();
  const { pageItems, controls } = paginate(items, { page, perPage: 5 });

  const rows = pageItems.map(item => [
    inlineButton(item.name, `select:${item.id}`),
  ]);

  const keyboard = inlineKeyboard([...rows, ...controls.inline_keyboard]);
  await ctx.editMessageText("Choose an item:", { reply_markup: keyboard });
}

bot.on("callback_query:data", async (ctx) => {
  const data = ctx.callbackQuery.data;
  if (data.startsWith("pg:next:")) await showPage(ctx, parseInt(data.split(":")[2]));
  if (data.startsWith("pg:prev:")) await showPage(ctx, parseInt(data.split(":")[2]));
  await ctx.answerCallbackQuery();
});

回调格式: <prefix>:prev:<n> / <prefix>:next:<n>,其中 n = 目标页索引。


4. Toolkit 与纯 grammY

任务纯 grammYToolkit
行内按钮{ text: "Hi", callback_data: "x" }inlineButton("Hi", "x")
URL 按钮{ text: "Link", url: "..." }urlButton("Link", "...")
复制文本按钮{ text: "Copy", copy_text: { text: "..." } }copyTextButton("Copy", "...")
Web App 按钮{ text: "Open", web_app: { url: "..." } }webAppButton("Open", "...")
网格菜单手动分行menuKeyboard(items, cols)
确认行手动创建含 2 个按钮的行confirmKeyboard("prefix")
分页手动切片并处理上一页/下一页逻辑paginate(items, opts)

Toolkit 构建器生成的 JSON 结构与 grammY 所需的完全相同。它们只是为了方便,并不会造成锁定。想减少样板代码时就使用它们;需要完全掌控时则跳过。


常见错误

  1. 在 keyboard 字段中使用 inline_keyboard——这是不同的对象。行内键盘放在 reply_markup.inline_keyboard 下;回复键盘放在 reply_markup.keyboard 下。
  2. 调用 paginate() 却没有处理器——按钮会生成 pg:prev:X / pg:next:X 数据。必须在回调处理器中处理这些数据。
  3. 未处理未知回调——始终为意外的回调数据提供备用的 answerCallbackQuery。
  4. 对新消息调用 editMessageText——你需要从先前的回复中获取 message_id。第一条消息使用 ctx.reply(),更新消息使用 ctx.editMessageText()。
  5. callback_data 超过 64 字节且包含非 ASCII 字符——Telegram 会以 BUTTON_DATA_INVALID 拒绝。计算的是字节数,不是字符数。参见 telegram-bot-api-fundamentals §4。
  6. 使用 ForceReply 却没有步骤筛选条件——聊天中的每条文本消息都会触发你的“等待 X”处理器。请通过 ctx.session.step 进行限制。参见 telegram-bot-ux §6。
  7. 回复键盘搭配 resize_keyboard: true,然后依赖它在桌面端生效——Telegram Desktop 5.3.2+ 会忽略 resize_keyboard。设计时最多使用 4 列。
  8. 缺少 input_field_placeholder——用户看到空输入框后会不知道该输入什么。请务必设置。
  9. 在单次提示中使用 one_time_keyboard: false(默认值)——用户点击后键盘仍会显示。对于“点击一次即完成”的流程,请设置 one_time_keyboard: true。
  10. 用户已分享联系人后仍使用自定义键盘请求联系人——旧键盘仍会显示。请发送带有 remove_keyboard: true 的新消息,或更新键盘。

评论

支持 Markdown