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。
對應到正確的傳送 API
| 接入方式 | 文字欄位 | 格式選項 |
|---|---|---|
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 的工作程序,應按渠道選擇渲染器。共用路由不等於共用格式語法。
加入重試前,先測試內容邊界
使用自行建立的測試資料,不使用真實客戶對話:
| 測試內容 | 確認事項 |
|---|---|
姓名含有 & 及尖括號 | 原始字元正常顯示,只有可信標籤產生格式 |
原文含有字面文字 & | 保留原文,不被悄悄解碼 |
| 表情符號、多語文字與換行 | 序列化與渲染不破壞內容 |
| 空輸入或很長的 AI 回答 | 傳送前執行驗證及長度策略 |
| 同一原文渲染兩次 | 每次從原文開始,不再轉義上次輸出 |
長內容應先按語意拆分原文,再逐段渲染。不要任意截斷已產生的 HTML 字串,否則可能切斷標籤或實體。請核對所選方法目前的長度規則;本例不實作長度驗證或富訊息功能。
已確認的格式拒絕需要修正內容,不是進入冷卻循環。流量限制請參照Telegram 429 傳送佇列指南。若逾時且未收到回應,保留「結果未知」,不要自動補傳純文字,以免重複傳送已送達的回覆。
常見問題
JSON 有效,就代表格式有效嗎?
不是。JSON 可以成功解析,但其中的文字仍可能違反所選格式規則。
遇到失敗都改用 MarkdownV2 嗎?
不應這樣做。MarkdownV2 有自己的轉義規則,應先選定模式,再使用對應的渲染器。
為甚麼純文字內出現 HTML 實體?
檢查是否在移除解析模式後繼續使用 HTML 轉義結果。純文字應從原始內容開始產生。
這個函式能處理整份 HTML 文件嗎?
不能。它只轉義可信標籤之間的動態文字,不驗證任意標籤、屬性、連結或訊息長度。
下一步與來源
分別實作純文字與 HTML 渲染器,再以受控 Telegram 對話測試。連接訊息帳戶時,請以UnifyPort 文字傳送契約為準。
資料核對日期:2026-10-21。
令訊息接入變成一條穩定嘅產品管線。
先用統一嘅 API 跑通發送,再用標準事件將所有入站訊息接返去業務系統。