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:只改模式、不改渲染逻辑,不能解决问题。
用一个受控会话和简短、无敏感信息的内容逐步验证:
- 检查 SDK 或自动化工具最终序列化的请求。工具默认值可能在你的代码之外添加解析模式。
- 不带格式选项发送原始短文本,不要复用已为 HTML 或 Markdown 转义过的字符串。
- 加入只含固定内容的简单 HTML 模板。
- 把动态值按 HTML 文本规则转义后,再插入模板。
- 分别检查 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('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>');
}
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&D <Team></b>
Use x < 5 & keep the original value.
用户应看到原始标点,姓名显示为粗体。标签结构由应用控制;客户姓名或 AI 回答里的标签应继续作为内容,而不是变成格式指令。
不要转义拼装后的整个模板,否则可信标签也会变成可见文本。也不要因为后面调用了 JSON.stringify 就省略 HTML 转义:JSON 序列化与 Telegram 格式解析是两层不同的处理。
如果需要链接,应另外实现经过审查的构造器,验证目标地址并安全处理属性。本例故意不插入动态属性。需要更多格式时,按官方支持的标签扩展并测试渲染器,不要直接接受任意 HTML。
映射到正确的发送接口
| 接入方式 | 文本字段 | 格式选项 |
|---|---|---|
Telegram 官方 Bot API sendMessage | text | parse_mode |
| UnifyPort 统一文本发送 | message.text | provider_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 的工作进程,应按渠道选择渲染器。统一路由不等于统一格式语法。
加入重试前,先测试内容边界
使用合成样例,不使用真实客户对话:
| 测试内容 | 验证目标 |
|---|---|
姓名包含 & 和尖括号 | 原始字符正常显示,只有可信标签产生格式 |
原文包含字面量 & | 保持原文,不被悄悄解码 |
| 表情、多语言和换行 | 序列化与渲染不破坏内容 |
| 空输入或很长的生成式回答 | 发送前执行校验及长度策略 |
| 同一原文渲染两次 | 每次从原文开始,不对上次结果再次转义 |
长内容应先按语义拆分原文,再逐段渲染。不要按任意字符位置截断已生成的 HTML,这可能切断标签或实体。长度规则应核对所选方法的当前文档;本例不实现长度校验或富消息功能。
已确认的格式拒绝需要修正内容,不需要冷却循环。限流请参照Telegram 429 发送队列指南。若超时且没有响应,应保留“结果未知”,不要自动再发纯文本替代品,以免重复发送已经送达的回复。
常见问题
JSON 合法,格式就一定合法吗?
不是。JSON 可以成功解析,但其中的文本仍可能违反所选格式规则。
是否应该把所有失败都改成 MarkdownV2?
不应该。MarkdownV2 有独立的转义规则,应先选择模式,再使用匹配的渲染器。
为什么纯文本里会显示 HTML 实体?
检查是否在移除解析模式后继续使用 HTML 转义结果。纯文本渲染应从原始内容开始。
此函数可以处理整份 HTML 文档吗?
不能。它只转义可信标签之间的动态文本,不验证任意标签、属性、链接或消息长度。
下一步与来源
分别实现纯文本和 HTML 渲染器,再用受控 Telegram 会话测试。连接消息账号时,以UnifyPort 文本发送契约为准。
资料核对日期:2026-10-21。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。