外部处理器

外部处理器是你的代码与 Telegram 之间的桥梁。当构建器的逻辑不够用时,你可以将某个反应交给自己的服务器处理:它接收事件并以 Telegram Bot API 格式回传命令,而 GetMyBot 负责传输、消息投递以及所有 Telegram 通信。

简介

GetMyBot 中的反应通过可视化方式搭建:触发器 → 条件 → 动作。但对于复杂或非标准逻辑——自有数据库、计算、调用第三方系统、ML、分支复杂的场景——在构建器中难以表达。外部处理器打破了这一上限:你接入自己的后端,并将整个反应的处理交给它。

外部服务从 GetMyBot 接收事件(触发器触发、用户回复文本、点击按钮),并命令机器人发送内容。GetMyBot 用自己的机器人令牌执行命令——在此之上运行着速率限制、重试、去重和对话日志。

工作模型

  • GetMyBot 是 WebSocket 服务器。 你的服务主动连接到它,无需公网地址或 webhook 端点。GetMyBot 不向你的服务发起任何出站连接,因此没有 SSRF 攻击面。
  • 通过集成令牌认证。 服务在连接时提交令牌;GetMyBot 与哈希值比对后保持连接。
  • 事件与命令采用 Telegram Bot API 格式。 服务接收事件,并以"方法 + 参数"数组的形式回复,与操作普通机器人完全一致。
  • 命令由 GetMyBot 用自己的令牌执行。 服务从不直接与 Telegram 通信:命令经过验证后进入 GetMyBot 的内部 outbox,由机器人令牌以速率限制、重试、去重和日志记录的方式发送。

一个连接服务于同一集成的所有对话——不同用户的事件在同一连接中多路复用,通过 session_id 字段区分。

配置集成

  1. 打开左侧菜单的集成,点击添加集成
  2. 选择外部处理器服务并设置名称。
  3. 创建后,卡片将显示:
    • WebSocket 地址 — 格式为 wss://api.mybot.app/ext/ws/<integrationID>,其中 <integrationID> 是此集成的 UUID。你的服务通过该地址连接。路径中缺少 id 则无法建立连接。
    • 连接令牌 — 仅在创建时显示一次。请复制并妥善保存;令牌只以哈希形式存储,无法再次查看。
    • 重新生成令牌 — 该按钮生成新令牌并立即使旧令牌失效。重新生成后请在你的服务中更新令牌。
    • 在线状态指示器 — 显示你的服务当前是否保持活跃连接。

影响协议的集成附加参数:

  • session_ttl_seconds — 会话生存时间(秒),默认 3600
  • on_unavailable_reaction_id — 备用反应:服务离线时触发(详见可靠性)。

绑定到反应

外部处理器不通过独立触发器接入,而是通过动作接入。在反应编辑器中添加转交外部处理器动作并选择对应集成。

任何现有的 GetMyBot 触发器都可以开启对话——命令、文本、按钮点击、链接参数、传入 Web 请求、定时任务。当此类触发器触发并到达该动作时,GetMyBot 会开启代理会话并向服务发送 session.open 事件。

如果此时服务未连接,GetMyBot 将启动集成设置中的备用反应on_unavailable_reaction_id)。若未设置备用反应——什么都不会发生(静默,用户不会收到错误)。

连接与认证

端点:GET /ext/ws/{integrationID},基于 WebSocket(wss)。integrationID 是"外部处理器"类型集成的 UUID。帧大小上限为 256 KiB。

认证方式有两种。

1. 握手时通过 Header 传入(推荐)。Authorization 头中传递令牌:

Authorization: Bearer <token>

2. 通过认证帧传入。 若未传入 Bearer 头,请在建立连接后 10 秒内发送第一帧:

{ "type": "auth", "token": "<token>" }

若 10 秒内未收到认证帧,连接以 4401 关闭。任何方式下令牌无效均以 4401 关闭。

令牌在集成创建时生成,仅显示一次,以哈希(sha256)形式存储,比对采用恒定时间算法。令牌轮换:

POST /api/bots/{botID}/integrations/{integrationID}/rotate-token

该端点返回新的明文令牌;旧令牌立即失效。

事件:机器人 → 服务

每个事件是一个 JSON 帧(EventEnvelope)。type 字段取值之一:session.openmessagecallbacksession.cancelsession.expired

信封完整字段:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Ivan",
    "username": "ivan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "原始 Telegram Update (raw JSON)" },
  "event": { "...": "GetMyBot 归一化事件 (raw JSON)" }
}
  • session_id — 会话(对话)标识符。
  • bot_id — 机器人标识符。
  • user — 用户数据:tg_user_id(int64)、可选的 first_nameusername,以及 params——用户参数(含链接参数),键值对映射。
  • chat — 聊天信息:id(int64)和 type(如 "private")。
  • update — 原始 Telegram Update(raw JSON)。在 session.openmessagecallback 时存在。
  • event — GetMyBot 归一化事件(raw JSON)。

各类型内容有所不同:

  • session.open — 触发器触发,新对话开启。包含 update(原始 Update)和 event(完整归一化事件),user.params 已填充。
  • message — 当服务持有"等待"状态(expect = text/any)且用户发送消息时到达。内容与 session.open 相同。
  • callback — 用户点击了服务之前发送的按钮。update 为原始值;user 中仅填充 tg_user_idevent 仅包含服务在按钮中设置的原始 callback_data:
{ "callback_data": "<服务在按钮中设置的原始值>" }
  • session.cancel / session.expired — 最小信封:session_idbot_iduser.tg_user_idchat.idcancel 还包含 chat.type)。无 update/event 字段。这些事件仅在服务在线时投递——不进入离线队列。

命令:服务 → 机器人

命令是一个 JSON 帧(CommandEnvelope)。type 字段取值之一:executesession.closeauthpingpong

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "你好" } }
  ],
  "expect": "none"
}
  • session_id — 命令所属的会话。
  • token — 仅用于 type: "auth"(详见连接与认证)。
  • methods{ "method": "<Telegram Bot API 方法名>", "params": { ... } } 对象数组。方法名与 Telegram Bot API 完全一致(如 sendMessage),params 为该方法的参数对象。
  • expect — 服务是否等待用户回复:"none""text""any"(空值视为 none)。
  • session.close — 显式关闭对话。

命令不直接发送到 Telegram:GetMyBot 验证后将其放入内部 outbox,再以机器人令牌发送(含速率限制、重试、去重、日志)。

execute 的处理方式

execute 命令在以下情况下整体丢弃:session_id 为空;会话未找到或状态不为 open;会话属于其他集成;会话已过期。

之后:

  • 速率限制: 每个会话每分钟最多 60 个 execute(滑动窗口)。超出限制则丢弃。
  • 单个 execute 最多 30 个方法;多余的静默丢弃。
  • 每个方法都检查白名单(详见允许的方法)——不在列表中的方法被拒绝。
  • 聊天守卫:params 中的 chat_id from_chat_id 值不为 0 且不等于会话的 chat_id,则整个方法被拒绝(不允许向其他聊天发送)。可以不填 chat_id:GetMyBot 会自动填入会话的聊天。
  • callback_data 的 inline 按钮会自动令牌化(内部格式 x:<token>);按钮生存时间 = 会话 TTL。url/webapp/switch_inline 类型按钮不受影响。

允许的方法

服务通过机器人令牌下发命令,因此方法集合受白名单限制——仅限发送消息及操作当前对话。不在列表中的方法静默忽略(不视为错误)。

  • 发送:sendMessagesendPhotosendDocumentsendVideosendAudiosendMediaGroupsendAnimationsendVoicesendLocationsendChatAction
  • 编辑与删除:editMessageTexteditMessageCaptioneditMessageReplyMarkupdeleteMessage
  • 响应点击:answerCallbackQuery
  • 转发与复制:forwardMessagecopyMessage
  • 置顶:pinChatMessageunpinChatMessage

账户级方法(setWebhookgetUpdateslogOutclosesetMyCommands 等)不被允许

等待与会话生命周期

每次 execute 之后,会话的走向取决于 expect 以及命令中是否包含带 callback_data 的 inline 按钮:

  • expect = textany — 用户的下一条消息将以 message 事件转发给服务。
  • expect = none 且命令中无 callback inline 按钮 — 会话自动关闭。这是"终止"消息。
  • expect = none,但有 callback 按钮 — 会话保持开启:按钮点击在令牌有效期内(TTL 内)被捕获。

重要:expect 不影响按钮点击的接收。 令牌化按钮的点击始终被捕获,与 expect 值无关——只要会话存活且按钮令牌有效。expect 只控制服务是否等待文本/任意回复。

对话边界

对话上下文的生命周期与会话保持一致:

  • 活跃等待时的文本回复expect = text/any)— 以 message 事件发给服务,不触发机器人的普通反应。
  • 点击令牌化按钮 — 以 callback 事件发给服务,包含原始 callback_data。GetMyBot 始终自动确认点击(answerCallbackQuery);验证点击归属(机器人 + 用户 + 会话聊天)。点击不关闭会话。
  • /cancel(以及 /cancel@bot 和"取消")在活跃的外部会话中 — 会话关闭,服务收到 session.cancel,用户收到"已取消。"。
  • 一个用户只有一个对话。 开启新会话会挤出同一用户此前的开启会话——对其发送 session.cancel
  • TTL 到期。 计时器到期后会话标记为 expired,若服务在线则发送 session.expired

限制与超时

  • 帧大小: 256 KiB。
  • 会话 TTL: 默认 3600 秒(可通过集成的 session_ttl_seconds 参数配置)。
  • 命令频率: 每个会话每分钟 60 个 execute(滑动窗口)。
  • 单个 execute 的方法数: 最多 30 个(多余的丢弃)。
  • 每个集成的开放会话数: 最多 1000 个。
  • 离线队列: 每个会话最多 100 个事件。
  • 心跳: 每 30 秒发送一次 ping;无活动超时 60 秒。
  • 认证帧超时: 10 秒。

可靠性(心跳、离线、队列、fallback)

  • 心跳。 服务器每 30 秒发送一帧 {"type":"ping"}。若客户端超过 60 秒无活动,连接关闭。客户端可自行发送 ping/pong 保持活跃。连接断开不会终止对话:会话在数据库中存活直到 TTL 到期或重新连接。
  • 重连由服务负责。 若连接断开,你的服务需主动重连。数据库中的开放会话在 TTL 到期前不受服务重启影响。
  • 启动时离线的 fallback。 若反应触发时服务未连接,则启动集成设置中的备用反应(on_unavailable_reaction_id)。若未设置,则什么都不发生(静默)。
  • 离线队列。 因离线未能投递的 message/callback 事件进入离线队列(最多 100 个,FIFO,存活至会话过期),重连后补发。session.open/session.cancel/session.expired 事件不进入队列
  • 投递保证——至多一次(at-most-once)。 队列溢出或过期时事件丢弃。请将逻辑设计为单个事件丢失不会破坏场景。

在线状态指示器

可通过以下端点检查服务是否保持活跃连接:

GET /api/bots/{botID}/integrations/{integrationID}/status

响应:

{ "online": true }

同样的指示器在控制台的集成卡片上也可查看。MCP 中的等效工具为 get_integration_status

安全性

GetMyBot 以自己的令牌执行第三方服务的命令,因此安全防护严格。

  • 无 SSRF 攻击面。 GetMyBot 作为 WebSocket 服务器,不向服务发起任何出站连接——是服务连接到 GetMyBot。平台不存在由外部控制的、会向某个地址发请求的 URL。
  • 令牌以哈希形式存储(sha256),比对采用恒定时间算法,支持令牌轮换。
  • 租户隔离。 命令严格绑定到自身集成;会话绑定到机器人 + 用户 + 聊天。其他机器人的事件不会进入你的 socket。
  • 方法白名单 + 强制 chat_id 防止机器人被用作向任意聊天发送消息的工具。
  • 机器人令牌不对外暴露 — 服务从不直接与 Telegram 通信。
  • 密钥脱敏。 集成令牌和敏感值在日志中隐藏。

示例

最简处理器:在对话开启时发送消息并关闭会话(expect: "none")。请替换集成 id 和令牌。地址中必须包含 <integrationID>

Node.js

import WebSocket from "ws";

const URL = `wss://api.mybot.app/ext/ws/${process.env.MYBOT_INTEGRATION_ID}`;
const ws = new WebSocket(URL, {
  headers: { Authorization: `Bearer ${process.env.MYBOT_TOKEN}` },
});

ws.on("message", (raw) => {
  const ev = JSON.parse(raw);
  if (ev.type === "session.open") {
    ws.send(JSON.stringify({
      type: "execute",
      session_id: ev.session_id,
      expect: "none",
      methods: [{ method: "sendMessage", params: { text: "来自外部处理器的问候!" } }],
    }));
  }
});

Python

import json, os, asyncio, websockets

async def main():
    url = f"wss://api.mybot.app/ext/ws/{os.environ['MYBOT_INTEGRATION_ID']}"
    headers = {"Authorization": f"Bearer {os.environ['MYBOT_TOKEN']}"}
    async with websockets.connect(url, additional_headers=headers) as ws:
        async for raw in ws:
            ev = json.loads(raw)
            if ev["type"] == "session.open":
                await ws.send(json.dumps({
                    "type": "execute",
                    "session_id": ev["session_id"],
                    "expect": "none",
                    "methods": [
                        {"method": "sendMessage", "params": {"text": "来自外部处理器的问候!"}}
                    ],
                }))

asyncio.run(main())

Inline 按钮与 callback 处理

要捕获按钮点击,请发送带有 callback_data 的按钮(GetMyBot 自动令牌化)。使用 expect: "none" 带按钮时,会话保持开启直到按钮令牌有效期结束——点击以 callback 事件到达,其中 event.callback_data 等于原始值。无需手动确认点击(answerCallbackQuery)——GetMyBot 自动处理。

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "请点击按钮",
        reply_markup: { inline_keyboard: [[{ text: "出发", callback_data: "go" }]] },
      },
    }],
  }));
} else if (ev.type === "callback" && ev.event.callback_data === "go") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{ method: "sendMessage", params: { text: "按钮已点击!" } }],
  }));
}

下一步

  • 集成 — 可接入服务的总览。
  • 反应 — 触发器、条件和动作的组合方式。
  • Web 请求与 webhook — 无需对话即可调用外部 URL 的更简单方式。