机器人开发
创建机器人
用 @BotFather 创建机器人,接收和回复消息,再到按钮、命令、照片和群组——从头到尾一步步跟着做的教程。
完成本教程后,你将拥有一个能回复消息、带按钮、会处理命令菜单和照片、还能在群组中工作的机器人。你需要准备 Pabal 应用、一台用来运行机器人程序的电脑,以及 Python 3.10 及以上或 Node.js 18 及以上两者之一。
机器人是如何工作的
Pabal 的机器人是由程序控制的账号。发给机器人的消息会存入机器人的消息箱,机器人程序向服务器询问“有新消息吗?”(getUpdates)并把它们取走,然后发送回复(sendMessage)。机器人程序在服务器之外——你自己的电脑上——运行。
图示说明
- 三条主线:左边是使用应用的用户,中间是 Pabal 服务器,右边是你的机器人程序。竖直的虚线表示时间流逝的方向。
- 灰色箭头是机器人主动发出的请求,蓝色箭头是由此往来的消息。机器人程序只负责发出请求,服务器不会主动联系机器人(除非使用 Webhook)。
- ①中的等待(长轮询)是关键:传入
timeout=30后,如果没有新的更新,服务器不会立刻返回空结果,而是最多保持 30 秒,在消息到来的那一刻(②)返回(③)。因此响应迅速,请求次数也少。 - ⑥中的 offset 就是“已收到”的标记:把最后处理的
update_id加 1 后发送,该编号及之前的更新就会从服务器上删除。如果不增大 offset,就会一直收到相同的更新。 - ⑤与用户发送的消息走同一条路:机器人的回复会推送到应用的所有设备,也会出现在对话列表中。
1. 用 @BotFather 创建机器人
机器人是通过与 Pabal 应用中的 @BotFather 对话来创建的。BotFather 是内置在 Pabal 服务器中的机器人。
- 在应用的搜索框中输入
BotFather,打开 BotFather。点开始后会收到命令列表。 - 发送
/newbot。 - 发送机器人的名字。这是显示在对话列表中的名字,也可以使用中文。
- 发送机器人的用户名。用户名由 5~32 个英文字母、数字和下划线组成,以英文字母开头,并且必须以
bot结尾。 - 收到包含令牌的回复就完成了。请把令牌复制保存好。
BotFather 目前用韩语回复。上面三条回复的意思依次是:请告诉它机器人的名字(显示在对话列表中的名字);接着请为机器人设定用户名;最后告诉你新机器人 @hello_test_bot 已创建、可以搜索它开始对话,并给出机器人令牌,提醒你像保管密码一样妥善保管令牌。
| BotFather 命令 | 作用 |
|---|---|
/newbot | 创建新机器人(名字 → 用户名 → 令牌) |
/mybots | 我创建的机器人列表 |
/token | 再次查看机器人令牌 |
/revoke | 重新签发令牌——旧令牌立即失效,用旧令牌建立的连接也会断开 |
/setcommands | 设置命令菜单(每行一条 命令 - 说明) |
/deletebot | 删除机器人——发送 네, 삭제합니다(意为“是的,删除”)进行确认。删除后用户名会被释放,可以再次使用 |
/cancel | 取消正在进行的操作 |
像 /token @hello_test_bot 这样在命令后面附上用户名发送,就会跳过“是哪个机器人?”这一步。
2. 管理令牌
令牌的形式是 <机器人 ID>:<密钥>。前面的数字是机器人的用户 ID,后面是密钥。只凭一个令牌就能完全控制机器人,所以请像对待密码一样对待它。
- 不要写在代码里,请放在环境变量(
BOT_TOKEN)或密钥存储中。不要上传到公开仓库。 - 如果泄露了,请向 BotFather 发送
/revoke。旧令牌会立即被拒绝(401 Unauthorized),用旧令牌连接在 MTProto 上的机器人会话也会被断开。 - 令牌包含在地址(URL)中,因此请不要让机器人程序在日志中记录请求地址。Pabal 服务器也不会在日志中记录 Bot API 地址。
3. 第一个请求——getMe
所有请求的地址都是 https://pabal.me/bot<令牌>/<方法>。先用 getMe 确认令牌是否正确。
export BOT_TOKEN='100003:AbCdEf…'
curl -s "https://pabal.me/bot$BOT_TOKEN/getMe"# pip install requests
import os
import requests
r = requests.get(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/getMe", timeout=10)
print(r.json())// Node.js 18 及以上——已内置 fetch
const res = await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/getMe`);
console.log(await res.json());成功时会返回如下内容。所有响应都是 JSON,包含 ok 和 result(成功时),或者 error_code 和 description(失败时)。
{
"ok": true,
"result": {
"id": 100003,
"is_bot": true,
"first_name": "你好机器人",
"username": "hello_test_bot",
"can_join_groups": true,
"can_read_all_group_messages": true,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
}
令牌错误时,会返回 HTTP 401 以及 {"ok": false, "error_code": 401, "description": "Unauthorized"}。
4. 接收消息——getUpdates
在应用中打开机器人,点开始或随便发一句话,然后获取新的更新。
curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates?timeout=30"
{
"ok": true,
"result": [
{
"update_id": 1,
"message": {
"message_id": 1,
"from": { "id": 100001, "is_bot": false, "first_name": "小明" },
"chat": { "id": 100001, "first_name": "小明", "type": "private" },
"date": 1789805661,
"text": "/start",
"entities": [ { "type": "bot_command", "offset": 0, "length": 6 } ]
}
}
]
}
- update_id:每个更新递增 1 的编号。处理完后,在下一次请求中传入
offset=update_id+1,该编号及之前的更新就会作为“已收到”被删除。 - timeout:没有新的更新时等待的秒数(0~50)。为 0 时立即返回空列表。建议使用 25~30。
- chat.id:发送回复的目标。一对一对话时是用户的 ID(正数),群组时是负数。
- 可以接收的更新有三种:
message(新消息)、edited_message(被修改的消息)、callback_query(按钮点击)。
尚未取走的更新,每个机器人最多在服务器内存中保留最近 1,000 条。服务器重启后,尚未取走的更新会丢失(消息本身仍保留在对话中)。不要让机器人长时间停止运行。
5. 发送回复——sendMessage
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" \
-H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "text": "你好!"}'requests.post(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/sendMessage",
json={"chat_id": 100001, "text": "你好!"}, timeout=10)await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: 100001, text: '你好!' }),
});参数可以用 JSON 正文、表单(application/x-www-form-urlencoded)、上传文件时的 multipart/form-data、地址后面的查询字符串中任意一种方便的方式发送。结果是发送出去的消息(Message)。
只有在用户至少给机器人发过一次消息之后,机器人才能给这个用户发消息。否则会返回 403 Forbidden: bot can't initiate conversation with a user。这与 Telegram 的规则相同。
6. 完成一个复读机器人
把接收和发送不断重复,就成了一个机器人。下面是不使用任何库写成的完整代码。
# echo.py — pip install requests
# 运行:BOT_TOKEN='100003:…' python3 echo.py
import os
import requests
API = f"https://pabal.me/bot{os.environ['BOT_TOKEN']}"
def call(method, **params):
r = requests.post(f"{API}/{method}", json=params, timeout=60)
data = r.json()
if not data["ok"]:
raise RuntimeError(f"{method}: {data['description']}")
return data["result"]
offset = 0
print("机器人已启动。按 Ctrl+C 停止")
while True:
for update in call("getUpdates", offset=offset, timeout=30):
offset = update["update_id"] + 1 # 标记为已收到
message = update.get("message")
if message and "text" in message:
call("sendMessage", chat_id=message["chat"]["id"], text=message["text"])// echo.mjs — Node.js 18 及以上,不使用库
// 运行:BOT_TOKEN='100003:…' node echo.mjs
const API = `https://pabal.me/bot${process.env.BOT_TOKEN}`;
async function call(method, params = {}) {
const res = await fetch(`${API}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await res.json();
if (!data.ok) throw new Error(`${method}: ${data.description}`);
return data.result;
}
let offset = 0;
console.log('机器人已启动。按 Ctrl+C 停止');
for (;;) {
const updates = await call('getUpdates', { offset, timeout: 30 });
for (const update of updates) {
offset = update.update_id + 1; // 标记为已收到
const message = update.message;
if (message?.text) {
await call('sendMessage', { chat_id: message.chat.id, text: message.text });
}
}
}7. 用库来开发
面向 Telegram 的机器人库都有更改服务器地址的设置。只改这一项设置,机器人就能原样在 Pabal 上运行。迁移为 Telegram 编写的机器人时也只需改这一项,令牌则从 Pabal 的 BotFather 重新获取即可。
| 库 | 要修改的设置 | 验证过的版本 |
|---|---|---|
| python-telegram-bot | .base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot") | 22.8 |
| aiogram | AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) | 3.31 |
| 不使用库(HTTP) | 地址开头的 https://api.telegram.org → https://pabal.me | — |
# hello_bot.py — pip install python-telegram-bot
# 运行:BOT_TOKEN='100003:…' python3 hello_bot.py
import os
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes,
MessageHandler, filters)
SERVER = "https://pabal.me"
def buttons():
return InlineKeyboardMarkup([[InlineKeyboardButton("👍 点赞", callback_data="like"),
InlineKeyboardButton("🔢 数字加一", callback_data="count")]])
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("你好!点一下按钮试试。", reply_markup=buttons())
async def button(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
if query.data == "like":
await query.answer("谢谢!") # 在点击者屏幕上短暂显示的文字
else:
n = context.chat_data.get("n", 0) + 1
context.chat_data["n"] = n
await query.answer() # 先回答
await query.edit_message_text(f"数字:{n}", reply_markup=buttons()) # 再修改消息
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"你说的是“{update.message.text}”。")
def main():
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.base_url(f"{SERVER}/bot") # 用 Pabal 代替 api.telegram.org
.base_file_url(f"{SERVER}/file/bot")
.build())
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(button))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
print("机器人已启动。按 Ctrl+C 停止")
app.run_polling()
if __name__ == "__main__":
main()# echo_aiogram.py — pip install aiogram
# 运行:BOT_TOKEN='100003:…' python3 echo_aiogram.py
import asyncio
import os
from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandStart
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message):
await message.answer("你好!随便发点什么试试。")
@dp.message()
async def echo(message):
if message.text:
await message.answer(message.text)
async def main():
session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) # Pabal
bot = Bot(os.environ["BOT_TOKEN"], session=session)
print("机器人已启动。按 Ctrl+C 停止")
await dp.start_polling(bot)
asyncio.run(main())8. 按钮与回调
要给消息加上内联按钮,请在 reply_markup 中传入 inline_keyboard(由行组成的数组,每一行又是按钮的数组)。按钮有两种:点击后通知机器人的 callback_data 按钮,以及打开链接的 url 按钮。
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -H 'Content-Type: application/json' -d '{
"chat_id": 100001,
"text": "请选择",
"reply_markup": {
"inline_keyboard": [
[ {"text": "👍 点赞", "callback_data": "like"}, {"text": "👎 不太好", "callback_data": "dislike"} ],
[ {"text": "打开 Pabal 文档", "url": "https://pabal.me/docs/"} ]
]
}
}'
用户点击 callback_data 按钮后,机器人会收到一个 callback_query 更新。
{
"update_id": 7,
"callback_query": {
"id": "5812039457730125441",
"from": { "id": 100001, "is_bot": false, "first_name": "小明" },
"message": { "message_id": 4, "chat": { "id": 100001, "type": "private", "first_name": "小明" }, "text": "请选择", … },
"chat_instance": "8413962145072395171",
"data": "like"
}
}
机器人必须在 10 秒内用 answerCallbackQuery 作出回答。在此期间,应用会在按钮上显示时钟图标并等待。
# 在屏幕上方短暂显示的文字(show_alert: true 时显示为确认对话框)
curl -s "https://pabal.me/bot$BOT_TOKEN/answerCallbackQuery" -H 'Content-Type: application/json' \
-d '{"callback_query_id": "5812039457730125441", "text": "谢谢!"}'
# 修改被点击消息的文字和按钮
curl -s "https://pabal.me/bot$BOT_TOKEN/editMessageText" -H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "message_id": 4, "text": "你点了赞 👍"}'
callback_data为 1~64 字节。一条消息最多可以有 100 个按钮。- 如果不回答回调,应用会在 10 秒后停止等待。如果机器人没有运行,服务器会立即结束等待。
- 只有第一次回答有效。有些库在修改消息时会先发送一个空回答,所以请先发送带文字的回答。
- 用相同的文字和相同的按钮修改时,会返回
400 Bad Request: message is not modified(与 Telegram 相同)。
输入框下方的键盘
传入 keyboard 后,输入框处会换成一块按钮面板,点击按钮就会把按钮上的文字作为消息发送出去。用 remove_keyboard 隐藏它,用 force_reply 开启回复模式。
{
"chat_id": 100001,
"text": "选哪个?",
"reply_markup": {
"keyboard": [ [ {"text": "是"}, {"text": "否"} ], [ {"text": "发送我的位置", "request_location": true} ] ],
"resize_keyboard": true,
"one_time_keyboard": true
}
}
9. 命令菜单
这是在对话窗口中按 / 或点击菜单按钮时出现的列表。可以用代码设置,也可以用 BotFather 的 /setcommands 设置。
curl -s "https://pabal.me/bot$BOT_TOKEN/setMyCommands" -H 'Content-Type: application/json' -d '{
"commands": [
{"command": "start", "description": "开始使用"},
{"command": "help", "description": "帮助"}
]
}'
命令由 1~32 个小写英文字母、数字和下划线组成,说明为 1~256 个字符,最多 100 条。传入 language_code 时会按语言分别保存列表,但目前应用中只显示未指定语言代码的默认列表。用户发送的 /start 等命令,会在消息的 entities 中以 bot_command 标注。
10. 收发照片
发送
用 multipart/form-data 上传文件,或者重新使用之前收到的照片的 file_id。照片最大 10MB,说明(caption)最多 1,024 个字符。暂不支持通过 URL 发送。
curl -s "https://pabal.me/bot$BOT_TOKEN/sendPhoto" \
-F chat_id=100001 -F caption='今日照片' -F photo=@sunset.jpgwith open("sunset.jpg", "rb") as f:
requests.post(f"{API}/sendPhoto", data={"chat_id": 100001, "caption": "今日照片"},
files={"photo": f}, timeout=60)接收
用户发送的照片会以消息中的 photo(按尺寸排列的列表,Pabal 只有一张原图)送达。用 getFile 获取路径后再下载。
# 1) file_id → file_path
curl -s "https://pabal.me/bot$BOT_TOKEN/getFile?file_id=AQAAAAAAAAB7…"
# {"ok":true,"result":{"file_id":"AQAA…","file_unique_id":"AQAA…","file_size":48213,"file_path":"photos/AQAA….jpg"}}
# 2) 下载——地址中包含 /file/
curl -s -o photo.jpg "https://pabal.me/file/bot$BOT_TOKEN/photos/AQAA….jpg"
11. 在群组中
- 在应用中创建群组时,或者在 群组信息 → 添加成员 中,搜索机器人的用户名并把它加进来。
- 加入群组的机器人会收到群组中的所有消息(相当于 Telegram 的“隐私模式关闭”)。
chat.type为"group",chat.id为负数。 - 用这个
chat.id调用sendMessage,消息就会发到群组中。按钮、照片、修改也与一对一对话完全相同。 - 机器人离开群组后,再向该群组发送会返回
403 Forbidden: bot is not a member of the group chat。
12. 改用 Webhook
如果机器人运行在拥有公开 HTTPS 地址的服务器上,就可以不用 getUpdates 去询问,而是让 Pabal 服务器把新的更新发送到这个地址。
curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' \
-d '{"url": "https://bot.example.com/pabal-webhook", "secret_token": "足够长的随机字符串"}'
设置、验证、重试以及在响应中回复等内容,请参阅 Webhook 文档。
通过 MTProto 连接的机器人(Telethon)
机器人也可以像应用一样通过 MTProto 连接。如果你已经在使用面向真人账号的工具(Telethon 等),这种方式会很方便。由于是同一个机器人账号,与 HTTP 混合使用也没问题。
# pip install telethon==1.42.0 ← 请使用 1.42(见下文说明)
# 服务器公钥:下载 https://pabal.me/docs/server-key.pem,放在同一个文件夹中
import asyncio
import os
from telethon import TelegramClient, events
from telethon.crypto import rsa
from telethon.sessions import StringSession
rsa.add_key(open("server-key.pem").read(), old=False) # Pabal 服务器的公钥
client = TelegramClient(StringSession(), api_id=1, api_hash="0" * 32)
client.session.set_dc(2, "122.34.175.215", 8443)
@client.on(events.NewMessage(incoming=True))
async def echo(event):
await event.reply(event.raw_text)
async def main():
await client.start(bot_token=os.environ["BOT_TOKEN"]) # auth.importBotAuthorization
print("机器人已启动。按 Ctrl+C 停止")
await client.run_until_disconnected()
asyncio.run(main())
- 请使用 Telethon 1.42。Pabal 使用 Layer 216,而更新版本的 Telethon 会尝试用更高的 Layer 解析响应,从登录开始就会失败(
TypeNotFoundError)。 - Pabal 不校验
api_id和api_hash,填任何值都可以。 - 对于 MTProto 机器人,服务器会实时推送新消息(不需要 getUpdates 或 Webhook)。回答回调时,请先调用
event.answer("…"),再调用event.edit(…)。
规则与限制
| 项目 | 值 |
|---|---|
| 消息字数 / 照片说明 | 4,096 个字符 / 1,024 个字符 |
| 照片大小(sendPhoto 上传) | 10MB |
| 请求正文大小 | 12MB |
getUpdates timeout · limit | 0~50 秒 · 1~100 条 |
| 未取走更新的保留 | 每个机器人保留最近 1,000 条,存放在服务器内存中(重启后丢失) |
| 等待回调回答 | 10 秒 |
callback_data · 按钮数量 | 1~64 字节 · 每条消息 100 个 |
| 命令 | 1~32 个小写英文字母、数字和下划线,说明 1~256 个字符,最多 100 条 |
| 主动发起对话 | 不可以——必须由用户先给机器人发消息 |
故障排除
| 症状 | 原因与解决方法 |
|---|---|
401 Unauthorized | 令牌错误,或者已通过 /revoke 更换。请向 BotFather 发送 /token 确认。 |
404 Not Found: method not found | Pabal 尚不支持该方法。请查看方法列表。 |
403 Forbidden: bot can't initiate conversation with a user | 该用户还从未与机器人对话过。请让对方在应用中打开机器人并点开始。 |
409 Conflict: can't use getUpdates method while webhook is active | 已设置 Webhook。请调用 deleteWebhook,或者改用 Webhook 接收。 |
| 点击按钮没有反应 | 机器人没有运行,或者没有调用 answerCallbackQuery。 |
| 一直收到相同的消息 | 没有增大 offset。请在下一次请求中传入已处理的 update_id + 1。 |
| 粗体、链接等格式不生效 | 尚不支持 parse_mode,文字会按原样发送。命令、@提及、URL、#标签 会自动显示为可点击。 |
| 机器人在群组中没有反应 | 机器人不是群组成员。请通过 群组信息 → 添加成员 把它加进来。 |