← 所有文章
教學

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。

對應到正確的傳送 API

接入方式文字欄位格式選項
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 跑通傳送,再用標準事件把所有入站訊息接回業務系統。