开发者文档
简体中文

机器人开发

创建机器人

用 @BotFather 创建机器人,接收和回复消息,再到按钮、命令、照片和群组——从头到尾一步步跟着做的教程。

完成本教程后,你将拥有一个能回复消息、带按钮、会处理命令菜单和照片、还能在群组中工作的机器人。你需要准备 Pabal 应用、一台用来运行机器人程序的电脑,以及 Python 3.10 及以上或 Node.js 18 及以上两者之一。

机器人是如何工作的

Pabal 的机器人是由程序控制的账号。发给机器人的消息会存入机器人的消息箱,机器人程序向服务器询问“有新消息吗?”(getUpdates)并把它们取走,然后发送回复(sendMessage)。机器人程序在服务器之外——你自己的电脑上——运行。

用户 (应用) Pabal 服务器 机器人程序 ① getUpdates?offset=0&timeout=30 最长等待 30 秒,直到有新的更新 ② 发送 "嗨" ③ [{update_id: 1, message: "嗨"}] ④ sendMessage(chat_id, "你好!") ⑤ 回复到达应用 (实时) ⑥ getUpdates?offset=2 — 1 号已收到,取下一个
用 getUpdates 接收、用 sendMessage 回复的基本顺序

图示说明

  • 三条主线:左边是使用应用的用户,中间是 Pabal 服务器,右边是你的机器人程序。竖直的虚线表示时间流逝的方向。
  • 灰色箭头是机器人主动发出的请求,蓝色箭头是由此往来的消息。机器人程序只负责发出请求,服务器不会主动联系机器人(除非使用 Webhook)。
  • ①中的等待(长轮询)是关键:传入 timeout=30 后,如果没有新的更新,服务器不会立刻返回空结果,而是最多保持 30 秒,在消息到来的那一刻(②)返回(③)。因此响应迅速,请求次数也少。
  • ⑥中的 offset 就是“已收到”的标记:把最后处理的 update_id 加 1 后发送,该编号及之前的更新就会从服务器上删除。如果不增大 offset,就会一直收到相同的更新。
  • ⑤与用户发送的消息走同一条路:机器人的回复会推送到应用的所有设备,也会出现在对话列表中。

1. 用 @BotFather 创建机器人

机器人是通过与 Pabal 应用中的 @BotFather 对话来创建的。BotFather 是内置在 Pabal 服务器中的机器人。

  1. 在应用的搜索框中输入 BotFather,打开 BotFather。点开始后会收到命令列表。
  2. 发送 /newbot
  3. 发送机器人的名字。这是显示在对话列表中的名字,也可以使用中文。
  4. 发送机器人的用户名。用户名由 5~32 个英文字母、数字和下划线组成,以英文字母开头,并且必须以 bot 结尾
  5. 收到包含令牌的回复就完成了。请把令牌复制保存好。
/newbot
BotFather새 봇을 만듭니다. 봇의 이름을 알려 주세요. (대화 목록에 보이는 이름이에요)
你好机器人
BotFather좋아요. 이제 봇의 사용자명을 정해 주세요. …
hello_test_bot
BotFather완료! 새 봇 @hello_test_bot 를 만들었어요. 검색해서 대화를 시작할 수 있어요. 봇 토큰: 100003:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789 토큰은 비밀번호처럼 안전하게 보관하세요. …

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,包含 okresult(成功时),或者 error_codedescription(失败时)。

{
  "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
aiogramAiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))3.31
不使用库(HTTP)地址开头的 https://api.telegram.orghttps://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.jpg
with 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_idapi_hash,填任何值都可以。
  • 对于 MTProto 机器人,服务器会实时推送新消息(不需要 getUpdates 或 Webhook)。回答回调时,请先调用 event.answer("…"),再调用 event.edit(…)

规则与限制

项目
消息字数 / 照片说明4,096 个字符 / 1,024 个字符
照片大小(sendPhoto 上传)10MB
请求正文大小12MB
getUpdates timeout · limit0~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 foundPabal 尚不支持该方法。请查看方法列表
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、#标签 会自动显示为可点击。
机器人在群组中没有反应机器人不是群组成员。请通过 群组信息 → 添加成员 把它加进来。