← 所有文章
教程

Telegram 媒体附件统一 Webhook:照片、语音、文档、联系人和位置怎么处理

Telegram 官方 Bot API 会把媒体内容拆成 Telegram 自己的 Message 字段,例如 photodocumentvoicecontactlocation。如果你接入的是 UnifyPort 统一 Webhook,就不需要把这些字段逐个写进业务模型。先存 message.received 标准事件:文本读 data.message.text,媒体读 data.message.attachments[],联系人读 data.message.contact,位置读 data.message.location

关键结论

  • Telegram 媒体消息不是“带文字的普通消息”;官方 Telegram Bot API 明确把照片、文档、语音、联系人、位置等内容作为不同可选字段呈现。
  • UnifyPort 会把 Telegram 入站内容归一为 message.received,同一个形状也用于 WhatsApp、LINE、TikTok、Zalo 和 X。
  • 媒体附件放在 data.message.attachments[],常见字段包括 typeurlmimetype,以及可选的大小或时长信息。
  • 读取正文、下载附件或进入 AI 分流之前,先用原始请求体验证 X-Device-Signature
  • 如果你还在比较 Telegram 官方 Bot API webhook 和统一入站路径,先看这篇接收路径对比

Telegram 发来的结构,和你的收件箱要存的结构

Telegram Bot API 的 Message 对象非常适合机器人开发:你直接按 Telegram 的字段分支处理。但跨境电商、东南亚运营团队、SaaS 客服队列通常要处理不止一个渠道。数据库更适合围绕“统一入站事件”建模,而不是围绕 Telegram 单个平台的对象建模。

UnifyPort 的 Standard event types and payload 文档给出的稳定事件外壳是:

{
  "id": "evt_b1a7c3e5f8",
  "type": "message.received",
  "provider": "telegram",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-06-08T12:37:00Z",
  "data": {
    "conversation": { "id": "5005", "type": "user" },
    "sender": { "id": "4004", "type": "user", "name": "Jordan Lee" },
    "message": {
      "id": "3003",
      "direction": "inbound",
      "sent_at": "2026-06-08T12:37:00Z",
      "contact": {
        "phone_number": "+8600000000000",
        "first_name": "Demo",
        "last_name": "User",
        "vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Demo User\nEND:VCARD",
        "user_id": 4004
      }
    },
    "event": { "kind": "message_received" }
  }
}

照片、语音、视频和文档进入 data.message.attachments[];联系人进入 data.message.contact;位置进入 data.message.location,其中包含 longitudelatitude

存储映射表

入站内容建议存储字段说明
文本或 captiondata.message.text不要假设每条媒体消息都有文字。
图片、音频、视频、文档、文件data.message.attachments[]每个附件有标准 type;媒体 URL 可能是临时地址,应按你的留存策略尽快处理。
语音/音频时长attachments[].duration_ms可能不存在,按可选字段处理。
文档标题attachments[].title可用于展示,不要当作唯一标识。
共享联系人data.message.contactTelegram 联系人可包含手机号、姓名、vCard 和 user id。
位置data.message.location把经纬度单独存储,不要只放在文本字段里。
消息标识data.message.id消息级动作和投影用它;投递去重用顶层 id

这篇文章是Webhook-first 入站集成清单的实现补充:先创建接收端,先存标准事件,再把附件分发给搜索、CRM、AI 工单或文件处理任务。

先验签,再解析

启用 signing_secret 后,UnifyPort 会发送 X-Device-TimestampX-Device-Signature。签名是下面内容的十六进制 HMAC-SHA256:

<X-Device-Timestamp>.<raw request body>

Node.js 官方文档说明了 crypto.createHmac()crypto.timingSafeEqual();Express 官方文档说明 express.raw() 可把请求体保留为 Buffer。接收端应先验签,再解析 JSON。

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;

app.post('/webhooks/unifyport', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestamp = req.get('X-Device-Timestamp') ?? '';
  const signature = req.get('X-Device-Signature') ?? '';
  const expected = crypto.createHmac('sha256', signingSecret).update(timestamp + '.').update(req.body).digest();
  const supplied = /^[0-9a-f]{64}$/i.test(signature) ? Buffer.from(signature, 'hex') : Buffer.alloc(0);
  if (supplied.length !== expected.length || !crypto.timingSafeEqual(supplied, expected)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  await storeEvent(event.id, event);
  if (event.type !== 'message.received' || event.provider !== 'telegram') return res.sendStatus(202);

  const message = event.data.message;
  for (const attachment of message.attachments ?? []) {
    await mediaQueue.enqueue({
      eventId: event.id,
      messageId: message.id,
      conversationId: event.data.conversation.id,
      type: attachment.type,
      url: attachment.url,
      mimetype: attachment.mimetype,
      title: attachment.title,
      durationMs: attachment.duration_ms
    });
  }
  if (message.contact) await contactQueue.enqueue(message.contact);
  if (message.location) await geoQueue.enqueue(message.location);
  return res.sendStatus(202);
});

生产环境中,把示例队列替换成你的数据库或任务系统。边界原则不变:慢下载、OCR、AI 摘要都不要发生在“可信入库”之前。验签、重试和幂等细节见 Webhook delivery and signature verification;如果你还要处理表情回应状态,可继续看统一 Webhook 处理 message.reaction

UnifyPort 适合放在哪里

UnifyPort 提供非官方接口,把已连接消息账号上的 Telegram 入站消息送成统一事件。你仍然负责决定媒体留存周期、是否复制临时文件、哪些 worker 可以访问附件。

好处是模型可以复用:同一个接收端今天处理 Telegram 图片,明天也能接收 WhatsApp 图片、LINE 照片、Zalo 消息、TikTok DM 或 X 消息,而不需要把 Telegram 的 Message 对象变成数据库中心。

限制与取舍

如果你要做 Telegram 机器人身份、bot command、inline keyboard、BotFather 配置或其他机器人特性,官方 Bot API 更合适。统一 Webhook 更适合“已有账号入站”和“多渠道队列”。另外,不要假设所有平台都有 Telegram 的每一种媒体形态;存储层统一,能力判断仍要放在边界处。

FAQ

Telegram 照片会放在 data.message.text 吗?

不会。文本和 caption 在 data.message.text;媒体文件在 data.message.attachments[],并带有标准附件 type

同一个 webhook 能接收联系人和位置吗?

可以。message.received 可以携带 data.message.contactdata.message.location 这类结构化字段。

是否要下载完媒体再返回 2xx?

通常不要。先存已验签事件,把媒体下载放进队列,然后返回 2xx。

这和 Telegram Bot API webhook 一样吗?

不一样。Telegram Bot API webhook 接收的是 bot token 对应机器人的 Update;UnifyPort webhook 接收的是已连接消息账号的标准事件,并可与其他平台共用接收端。

下一步

打开 Create webhook endpoint,订阅 message.received,启用 signing_secret,并分别测试文本、附件、联系人和位置消息。

Sources checked on 2026-09-08

UnifyPort API

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

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