PAYATHON 2026

Telegram 添加 Inline Keyboard 后如何保留评论按钮或 Thread View?

技术老齐

结论

Bot API 没有“强制保留原生评论按钮”的参数。如果客户端因 inline_keyboard 隐藏了原生的 “Leave a comment” 入口,增加管理员权限或调整按钮文字都无法让它重新出现。

可以在自定义键盘中添加 URL 按钮,指向讨论群中自动转发消息的 thread,也就是这条频道帖子的评论区。顶部原帖和回复列表的具体布局由 Telegram 客户端决定。

两种跳转方式需要区分:

  • 打开用户已有权限访问的讨论群线程:使用讨论群消息链接,加上 thread 参数。
  • 从公开频道帖子跳转到某条评论:使用频道帖子链接,加上 comment 参数,参数值必须是真实的评论消息 ID。

自定义按钮可以保留线程导航功能,但无法完整复制原生按钮的评论计数、头像和所有访问行为。

先分清三个消息 ID

频道关联 Discussion Group 后,频道帖子会自动转发到讨论群。这条自动转发消息的回复线程,就是该帖子的评论区。Telegram 讨论群文档

以这组消息为例:

位置消息示例 ID
Public Channel原始频道帖子123
Discussion Group自动转发的频道帖子,也是线程根消息456
Discussion Group用户对该帖子的某条评论789

这三个 ID 对应的位置或用途不同,不能互换。对于这里的普通消息线程,thread ID 就是讨论群根消息的 ID,即 456。Telegram 消息线程文档

此前尝试的几个链接有以下问题:

  • https://t.me/ChannelName/123?comment=1:1 表示讨论群中的评论消息 ID,不是“开启评论”的布尔值。如果它不是该帖子下的有效评论,就无法正确定位。
  • https://t.me/ChannelName/123?discussion:官方消息链接格式没有定义 discussion 参数,不能依赖它打开评论区。
  • https://t.me/ChannelName/1?thread=123:路径中的 1 仍然是要打开的消息 ID。thread 参数也不能把频道帖子 ID 自动转换为讨论群根消息 ID。Telegram 消息链接规范

用自定义按钮打开讨论线程

公开 Discussion Group

链接使用讨论群的 username 和群内根消息 ID:

https://t.me/DiscussionGroupName/456?thread=456

路径中的 456 指向根消息,thread=456 指定它所属的线程。DiscussionGroupName 应填写讨论群的 username,不要填写频道的 username。

私有 Discussion Group

使用 t.me/c/ 链接:

https://t.me/c/1234567890/456?thread=456

如果 Bot API 返回的讨论群 chat.id 是:

-1001234567890

链接中的内部 ID 就是:

1234567890

这类链接不会授予用户私有群的访问权限。用户通常需要已加入该群,或满足相应的访问条件。即使 Bot 是群管理员,点击按钮的用户也未必能访问。

这两种格式都是官方定义的消息链接,thread 参数用于指定消息线程。Telegram 消息链接规范

从频道帖子定位到已有评论

如果已知某条真实评论在群内的消息 ID,可以使用:

https://t.me/ChannelName/123?comment=789

其中:

  • 123 是频道帖子 ID。
  • 789 是该帖子下某条评论在讨论群中的消息 ID。

这个链接会请求进入该帖子的评论区,并定位到指定评论,因此适用于已有评论的情况。没有评论时,不应随意填入 1 或其他猜测值。Telegram 的 comment 参数定义

如何拿到讨论群根消息 ID

sendMessage 向频道发帖后返回的是频道消息,无法直接从返回值中取得讨论群根消息 ID。

Bot 需要监听 Discussion Group 的 message 更新,找到同时满足以下条件的消息:

  1. is_automatic_forward 为 true。
  2. forward_origin.type 为 "channel"。
  3. forward_origin.chat.id 是目标频道 ID。

然后保存映射:

(频道 ID, 频道帖子 ID) -> (讨论群 ID, 讨论群根消息 ID)

对应字段是:

channel_id = message["forward_origin"]["chat"]["id"]
channel_post_id = message["forward_origin"]["message_id"]

discussion_chat_id = message["chat"]["id"]
discussion_root_id = message["message_id"]

is_automatic_forward 标记频道帖子向关联讨论群的自动转发,MessageOriginChannel 包含来源频道及其消息 ID。Bot API Message 文档、MessageOriginChannel 文档

Python Requests 示例

这个函数接收 Bot API 原始 Update,收到目标讨论群的自动转发消息后,为对应的频道帖子设置“投票 + 评论”键盘。

示例中的投票按钮只是占位,投票逻辑仍需由现有的 callback_query 处理器完成。

import os
import requests

API = f"https://api.telegram.org/bot{os.environ['BOT_TOKEN']}"

CHANNEL_ID = -1001111111111
DISCUSSION_ID = -1002222222222


def api_call(method, payload):
    response = requests.post(
        f"{API}/{method}",
        json=payload,
        timeout=20,
    )
    response.raise_for_status()
    data = response.json()
    if not data.get("ok"):
        raise RuntimeError(data.get("description", "Bot API request failed"))
    return data["result"]


def handle_discussion_forward(update):
    message = update.get("message")
    if not message or message["chat"]["id"] != DISCUSSION_ID:
        return

    if not message.get("is_automatic_forward"):
        return

    origin = message.get("forward_origin", {})
    if origin.get("type") != "channel":
        return
    if origin["chat"]["id"] != CHANNEL_ID:
        return

    post_id = origin["message_id"]
    root_id = message["message_id"]
    chat = message["chat"]

    if chat.get("username"):
        base_url = f"https://t.me/{chat['username']}"
    else:
        chat_id = str(chat["id"])
        if not chat_id.startswith("-100"):
            raise ValueError("Expected a supergroup chat ID")
        base_url = f"https://t.me/c/{chat_id[4:]}"

    comment_url = f"{base_url}/{root_id}?thread={root_id}"

    # 生产环境应在这里持久化频道帖子与讨论群根消息的映射。
    keyboard = [
        [
            {"text": "赞成", "callback_data": f"vote:yes:{post_id}"},
            {"text": "反对", "callback_data": f"vote:no:{post_id}"},
        ],
        [{"text": "评论", "url": comment_url}],
    ]

    return api_call(
        "editMessageReplyMarkup",
        {
            "chat_id": CHANNEL_ID,
            "message_id": post_id,
            "reply_markup": {"inline_keyboard": keyboard},
        },
    )

频道帖子可以先带投票键盘发布,收到讨论群的自动转发更新后,再补上评论按钮。也可以先发帖,等映射建立后,一次性设置完整键盘。

editMessageReplyMarkup 会替换整套 inline keyboard,所以更新投票数字时,也要一并传入评论按钮。评论按钮使用 url 字段,投票按钮继续使用 callback_data。Bot API 按钮定义、editMessageReplyMarkup

使用 python-telegram-bot 时,处理流程相同,只需改为通过对象属性读取消息字段,并调用对应的编辑键盘方法。

实施时需要注意什么

订阅讨论群消息更新

如果显式配置了 allowed_updates,至少应包含 "message";现有投票逻辑还需要 "callback_query"。只接收 "channel_post",无法取得讨论群自动转发消息的 ID。

自动转发更新与频道发帖请求的返回存在时序差异。保存映射时,应使用频道 ID 和帖子 ID 作为键,避免依赖固定等待时间。

私有群链接的访问限制

Telegram 客户端可以通过 messages.getDiscussionMessage,在无需实际加入讨论群的情况下获取频道对应的讨论消息。这个方法属于 Telegram 的 MTProto API,不是可以用 Requests 直接调用的 Bot API 方法。messages.getDiscussionMessage 文档

因此,直接指向私有讨论群的 t.me/c/ 链接,无法保证未入群用户获得与原生评论入口完全相同的体验。如果要求所有频道读者都能使用,应分别用已入群和未入群的账号验证。已有真实评论时,也可以验证频道的 ?comment=<comment_id> 链接。

不要删除自动转发的根消息

删除讨论群中的自动转发消息,会关闭对应频道帖子的评论区。自定义 URL 按钮无法恢复已被删除的线程根消息。Telegram 讨论群文档

验证各客户端的跳转结果

链接能打开,还需要检查是否跳转到了预期位置。至少应在目标用户使用的移动端和桌面端验证:点击后是否进入该帖子的回复列表、顶部是否显示对应原帖,以及私有群访问条件是否符合预期。

如果必须保留原生评论按钮的完整交互,可以让原频道帖子不带自定义 inline keyboard,将投票放到另一条消息中。Bot API 没有强制同一条帖子同时显示两种入口的开关。

备注:内容仅供参考。