← 所有文章
教程

Telegram 格式解析失败:安全发送带 HTML 的动态文本

Telegram 消息使用固定文本时正常,插入客户姓名或 AI 回答后却失败,应先检查格式解析模式,而不是直接重试。发送纯文本时,省略 parse_mode 并使用原文;发送 HTML 时,先转义动态内容,再放入可信模板。JSON 编码正确,不代表文本也符合 Telegram 的格式规则。

核心要点

  • 纯文本、HTML 和 MarkdownV2 是不同的渲染方式,不能只换模式名称。
  • 分开保存原始内容与渲染结果,在输出边界只转义一次。
  • 已确认的格式拒绝、限流和结果未知的发送请求,需要不同处理。
  • UnifyPort 使用 provider_data.parse_mode,不是 Bot API 的顶层参数。

先定位格式解析这一层

Telegram Bot API 文档说明,parse_mode 用于解析文本实体,HTML 模式只支持规定的标签,并不是浏览器 HTML 渲染器。文档也区分旧版 Markdown 与 MarkdownV2:只改模式、不改渲染逻辑,不能解决问题。

用一个受控会话和简短、无敏感信息的内容逐步验证:

  1. 检查 SDK 或自动化工具最终序列化的请求。工具默认值可能在你的代码之外添加解析模式。
  2. 不带格式选项发送原始短文本,不要复用已为 HTML 或 Markdown 转义过的字符串。
  3. 加入只含固定内容的简单 HTML 模板。
  4. 把动态值按 HTML 文本规则转义后,再插入模板。
  5. 分别检查 API 返回结果和客户端实际显示。

如果纯文本也失败,应按实际错误检查目标、授权和长度,不要直接归因于格式。若你把发送操作嵌在 webhook 响应中,先阅读独立 sendMessage 请求与 webhook 响应的区别:后者无法提供可检查的发送结果。

转义动态内容,不接收任意标签

Telegram 要求 HTML 模式下不属于标签或实体的 <、>、& 使用对应 HTML 实体。下面的 JavaScript 示例只处理插入标签之间的文本,不是通用 HTML 清洗器、Markdown 转换器,也不验证链接属性。

function escapeTelegramHtmlText(value) {
  if (typeof value !== 'string') {
    throw new TypeError('Expected text');
  }
  return value
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;');
}

function renderSupportAnswer(customerName, answer) {
  return `<b>${escapeTelegramHtmlText(customerName)}</b>\n` +
    escapeTelegramHtmlText(answer);
}

const rendered = renderSupportAnswer(
  'R&D <Team>',
  'Use x < 5 & keep the original value.'
);

先替换 &,是为了避免再次转义后续替换生成的实体。预期生成的源码为:

<b>R&amp;D &lt;Team&gt;</b>
Use x &lt; 5 &amp; keep the original value.

用户应看到原始标点,姓名显示为粗体。标签结构由应用控制;客户姓名或 AI 回答里的标签应继续作为内容,而不是变成格式指令。

不要转义拼装后的整个模板,否则可信标签也会变成可见文本。也不要因为后面调用了 JSON.stringify 就省略 HTML 转义:JSON 序列化与 Telegram 格式解析是两层不同的处理。

如果需要链接,应另外实现经过审查的构造器,验证目标地址并安全处理属性。本例故意不插入动态属性。需要更多格式时,按官方支持的标签扩展并测试渲染器,不要直接接受任意 HTML。

映射到正确的发送接口

接入方式文本字段格式选项
Telegram 官方 Bot API sendMessagetextparse_mode
UnifyPort 统一文本发送message.textprovider_data.parse_mode

UnifyPort 的非官方接口连接消息账号,请求结构与 Bot API 不同。文本发送文档定义了 POST /v1/messages、非空 message.text 及渠道扩展 parse_mode。消息能力矩阵将此扩展限定为 Telegram,并不承诺各渠道都支持 HTML。

下面的构造器使用上面的渲染函数。传入实际消息账号 ID、选中会话的 ID 和类型。它只构造 JSON,不执行发送或重试,也不是已完成的集成测试。

function buildTelegramReply({ accountId, conversation, name, answer }) {
  return {
    account_id: accountId,
    to: { id: conversation.id, type: conversation.type },
    message: {
      type: 'text',
      text: renderSupportAnswer(name, answer)
    },
    provider_data: { parse_mode: 'HTML' }
  };
}

通过文档规定的接口发送请求体,在服务端用 X-Api-Key 鉴权。纯文本版本应省略 provider_data.parse_mode,并使用原始回答,而不是 HTML 渲染结果。没有明确映射时,不要把 Bot API 的 chat_id、entities 等参数放入统一请求体。

面向东南亚客户、同时处理 Telegram、LINE 或 WhatsApp 的工作进程,应按渠道选择渲染器。统一路由不等于统一格式语法。

加入重试前,先测试内容边界

使用合成样例,不使用真实客户对话:

测试内容验证目标
姓名包含 & 和尖括号原始字符正常显示,只有可信标签产生格式
原文包含字面量 &amp;保持原文,不被悄悄解码
表情、多语言和换行序列化与渲染不破坏内容
空输入或很长的生成式回答发送前执行校验及长度策略
同一原文渲染两次每次从原文开始,不对上次结果再次转义

长内容应先按语义拆分原文,再逐段渲染。不要按任意字符位置截断已生成的 HTML,这可能切断标签或实体。长度规则应核对所选方法的当前文档;本例不实现长度校验或富消息功能。

已确认的格式拒绝需要修正内容,不需要冷却循环。限流请参照Telegram 429 发送队列指南。若超时且没有响应,应保留“结果未知”,不要自动再发纯文本替代品,以免重复发送已经送达的回复。

常见问题

JSON 合法,格式就一定合法吗?

不是。JSON 可以成功解析,但其中的文本仍可能违反所选格式规则。

是否应该把所有失败都改成 MarkdownV2?

不应该。MarkdownV2 有独立的转义规则,应先选择模式,再使用匹配的渲染器。

为什么纯文本里会显示 HTML 实体?

检查是否在移除解析模式后继续使用 HTML 转义结果。纯文本渲染应从原始内容开始。

此函数可以处理整份 HTML 文档吗?

不能。它只转义可信标签之间的动态文本,不验证任意标签、属性、链接或消息长度。

下一步与来源

分别实现纯文本和 HTML 渲染器,再用受控 Telegram 会话测试。连接消息账号时,以UnifyPort 文本发送契约为准。

资料核对日期:2026-10-21。

UnifyPort API

让消息接入变成一条稳定的产品管线。

先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。