关于 telegram-bot-ui
telegram-bot-ui 是用于构建 Telegram 机器人键盘及处理交互的技能,说明按钮、命令和 ForceReply 的适用场景,并提供菜单、确认和分页构建器。
主要功能
- 01讲解内联键盘和回复键盘
- 02选择按钮或文本输入方式
- 03构建菜单和确认键盘
- 04创建分页键盘控件
- 05支持 URL、复制文本和 Web App 按钮
- 06介绍 grammY 回调处理
适用场景
- 01添加按钮式主菜单
- 02使用 ForceReply 收集自由文本
- 03构建确认和分页流程
- 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
| 任务 | 纯 grammY | Toolkit |
|---|---|---|
| 行内按钮 | { 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 所需的完全相同。它们只是为了方便,并不会造成锁定。想减少样板代码时就使用它们;需要完全掌控时则跳过。
常见错误
- 在
keyboard字段中使用inline_keyboard——这是不同的对象。行内键盘放在reply_markup.inline_keyboard下;回复键盘放在reply_markup.keyboard下。 - 调用
paginate()却没有处理器——按钮会生成pg:prev:X/pg:next:X数据。必须在回调处理器中处理这些数据。 - 未处理未知回调——始终为意外的回调数据提供备用的
answerCallbackQuery。 - 对新消息调用
editMessageText——你需要从先前的回复中获取message_id。第一条消息使用ctx.reply(),更新消息使用ctx.editMessageText()。 callback_data超过 64 字节且包含非 ASCII 字符——Telegram 会以BUTTON_DATA_INVALID拒绝。计算的是字节数,不是字符数。参见telegram-bot-api-fundamentals§4。- 使用 ForceReply 却没有步骤筛选条件——聊天中的每条文本消息都会触发你的“等待 X”处理器。请通过
ctx.session.step进行限制。参见telegram-bot-ux§6。 - 回复键盘搭配
resize_keyboard: true,然后依赖它在桌面端生效——Telegram Desktop 5.3.2+ 会忽略resize_keyboard。设计时最多使用 4 列。 - 缺少
input_field_placeholder——用户看到空输入框后会不知道该输入什么。请务必设置。 - 在单次提示中使用
one_time_keyboard: false(默认值)——用户点击后键盘仍会显示。对于“点击一次即完成”的流程,请设置one_time_keyboard: true。 - 用户已分享联系人后仍使用自定义键盘请求联系人——旧键盘仍会显示。请发送带有
remove_keyboard: true的新消息,或更新键盘。