跳到主要内容

批量发送人与机器人会话中机器人消息

第三方应用通过本接口,以指定内部应用机器人的身份,向一个或多个目标成员批量发送单聊消息(文本消息或结构化卡片消息)。

服务端会先为每个目标成员批量创建或复用机器人单聊会话,再并发执行消息投递,并按目标用户返回逐项结果。

接口信息​

项目说明
接口名称批量发送人与机器人会话中机器人消息
权限标识robot.push(机器人消息推送)
请求方式POST
请求地址https://{gateway_host}/open/v1/robot/messages
数据格式application/json
鉴权方式应用级 access_token(需绑定 robot.push 权限)
幂等控制请求头 Idempotency-Key(必填)

注意:本接口是第三方应用机器人通道,使用应用级 access_token 鉴权。Webhook 自定义机器人是另一条独立通道(使用加签方案),两者的鉴权凭证不能混用。

前置条件​

  1. 应用状态:应用在管理后台「内部应用」中已发布版本且处于启用状态。
  2. 机器人启用:应用详情「机器人」面板中已开启机器人,且获取并记录了 robotCode。
  3. 权限与凭证:已获取有效的应用级凭据 access_token,且该令牌具备 robot.push 权限。
  4. 用户可见范围:目标接收人(userids)必须处于该应用的可见范围内,超出可见范围的用户会被逐项拦截。

应用机器人接入步骤​

应用机器人以应用身份向成员发送单聊消息,使用应用 access_token 和 robotCode。适合告警、待办和业务通知。

接入步骤​

  1. 在应用配置中开启机器人能力并发布应用。
  2. 获取服务端使用的 robotCode,不要将它下发到客户端。
  3. 获取应用 access_token。
  4. 按本接口发送消息;传入多个目标用户即为批量发送。

应用机器人支持 text 和 card 消息;消息内容和卡片字段见消息类型与卡片。

四语言最小示例​

Shell​

curl -sS -X POST "https://{gateway_host}/open/v1/robot/messages" \
-H "Authorization: Bearer access-token-xxx" -H "Content-Type: application/json" \
-H "Idempotency-Key: robot-demo-001" \
-d '{"robotCode":"robot_xxx","userids":["user-001"],"msgType":"text","content":{"text":"服务通知"}}'

PHP​

<?php
// 中文注释:机器人令牌和幂等键由服务端生成并保存。
$ch = curl_init('https://{gateway_host}/open/v1/robot/messages');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer access-token-xxx', 'Content-Type: application/json', 'Idempotency-Key: robot-demo-001'], CURLOPT_POSTFIELDS => json_encode(['robotCode' => 'robot_xxx', 'userids' => ['user-001'], 'msgType' => 'text', 'content' => ['text' => '服务通知']], JSON_THROW_ON_ERROR), CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch); curl_close($ch);

Golang​

package main

import ("bytes"; "net/http")

func main() {
// 中文注释:重试时保持 Idempotency-Key 和请求体不变。
body := []byte(`{"robotCode":"robot_xxx","userids":["user-001"],"msgType":"text","content":{"text":"服务通知"}}`)
request, _ := http.NewRequest(http.MethodPost, "https://{gateway_host}/open/v1/robot/messages", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer access-token-xxx"); request.Header.Set("Content-Type", "application/json"); request.Header.Set("Idempotency-Key", "robot-demo-001")
response, _ := http.DefaultClient.Do(request); defer response.Body.Close()
}

C++​

#include <curl/curl.h>

int main() {
// 中文注释:生产代码应读取逐项投递结果并记录 request id。
CURL* handle = curl_easy_init();
const char* body = R"({"robotCode":"robot_xxx","userids":["user-001"],"msgType":"text","content":{"text":"服务通知"}})";
struct curl_slist* headers = nullptr; headers = curl_slist_append(headers, "Authorization: Bearer access-token-xxx"); headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "Idempotency-Key: robot-demo-001");
curl_easy_setopt(handle, CURLOPT_URL, "https://{gateway_host}/open/v1/robot/messages"); curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(handle, CURLOPT_POSTFIELDS, body);
const CURLcode result = curl_easy_perform(handle); curl_slist_free_all(headers); curl_easy_cleanup(handle); return result == CURLE_OK ? 0 : 1;
}

请求参数​

请求头​

请求头必填说明
Content-Type是固定为 application/json
Authorization二选一Bearer <access_token>,推荐的令牌携带方式
access_token(Query 参数)二选一备选方式:在 URL Query 中携带,如 ?access_token=xxx
X-Request-Id是请求追踪关联 ID,必须全局唯一且非空白,响应体 requestId 中将原样回显
Idempotency-Key是客户端生成的幂等键,保证重试不产生重复消息

约束规则:

  • 令牌只能通过 Authorization 请求头或 Query 参数其一携带;若同时传递且值不一致将拒绝请求。
  • X-Secret 内部共享密钥由受信网关自动注入,调用方严禁自行伪造。

请求体​

{
"robotCode": "robot_7f3c1a2b-0d4e-4f5a-9b6c-8d7e6f5a4b3c",
"userids": ["10001", "10002"],
"msg": {
"mtype": "text",
"content": "您的验证码是 482913,5 分钟内有效。"
}
}
参数类型必填说明
robotCodestring是应用机器人的唯一调用标识
useridsstring[]是接收消息的用户 ID 列表(正整数字符串,不可重复,非空)
msgobject是消息主体对象
msg.mtypestring是消息类型:text(文本)或 card(结构化卡片)
msg.contentstring条件必填text 类型时必填;card 类型时可选,卡片渲染与离线通知文案都取自 msg.ext
msg.extobject条件必填card 类型时必填,必须为 JSON 对象(传 JSON 字符串会被拒绝)

字段约束:

  • userids 必须使用字符串数组传输,避免整数精度丢失。单次请求中不能出现重复 ID,且必须是正整数。

消息类型与卡片结构​

文本消息(text)​

mtype 为 text,正文通过 content 传入:

{
"robotCode": "robot_xxx",
"userids": ["10001"],
"msg": {
"mtype": "text",
"content": "系统将在今晚 22:00 进行升级维护。"
}
}

结构化卡片消息(card)​

mtype 为 card,卡片结构通过 msg.ext 传入,必须是 JSON 对象:

{
"robotCode": "robot_xxx",
"userids": ["10001", "10002"],
"msg": {
"mtype": "card",
"content": "",
"ext": {
"title": "版本发布",
"content": "版本发布窗口临近,请完成最后一轮检查并同步相关同学。",
"img": "https://xxx.png",
"button": [
{
"type": "normal",
"text": "稍后处理",
"actionType": "url",
"action": "https://example.com/release-notes"
}
]
}
}
}

卡片字段说明(msg.ext)​

字段类型必填说明
titlestring否卡片大标题,最长 64 个字符
contentstring否卡片正文介绍,最长 2000 个字符
imgstring否卡片封面配图 URL,非空时必须为合法的绝对 HTTP/HTTPS 地址
buttonobject[]否操作按钮数组(最多支持 3 个按钮)
button[].typestring是按钮样式,normal(普通按钮)或 primary(主按钮)
button[].textstring是按钮文本
button[].actionTypestring是按钮动作类型:url(跳转链接)或 none(无操作/纯展示)
button[].actionstring条件必填按钮跳转 URL,actionType="url" 时必填

字段约束:

  • msg.ext.title 与 msg.ext.content 是卡片的文字描述,按 Unicode 字符计数,缺失或显式为 null 视为未配置。两个字段展示位不同,各自使用独立上限:title 最长 64 个字符, content 最长 2000 个字符。
  • 单个字段超过各自上限时整批请求被拒绝,返回 card title is too long 或 card content is too long。
  • 文字字段传入数字、对象、数组等非字符串取值时同样整批拒绝,返回 invalid card title 或 invalid card content,避免绕过长度限制。

离线推送文案:

  • 接收方离线时通知栏文案优先取 msg.ext.title,标题为空取 msg.ext.content, 两者都为空时使用兜底文案 你有一条新消息(按 80 个字符截断展示)。

消息类型与卡片字段详解​

说明应用机器人推送消息的 mtype、content 与 ext 结构,以及客户端展示规则。

本文只覆盖开放接口(POST /open/v1/robot/messages)支持的消息类型, 即 text 与 card 两种。接口调用方式、鉴权与错误码见《机器人文档》。

消息结构​

开放接口中,消息通过 msg 对象传递:

{
"mtype": "text",
"content": "消息正文",
"ext": {}
}
字段类型说明
mtypestring消息类型,text 或 card
contentstring文本内容,mtype=text 时必填;mtype=card 时不使用
extobject结构化扩展,mtype=card 时必填;mtype=text 时不使用

文本消息​

最简形式,适合验证码、状态提醒等纯文本通知。

{
"mtype": "text",
"content": "您的验证码是 482913,5 分钟内有效。"
}
约束说明
content 必填为空时整批请求返回 content is required
长度无强制上限,建议控制在客户端可完整展示的范围内

卡片消息​

卡片用于承载带标题、正文、配图和跳转按钮的结构化通知。

外层结构​

mtype=card 时,卡片内容全部放在 ext 中,ext 必须是 JSON 对象:

{
"mtype": "card",
"content": "",
"ext": {
"title": "版本发布",
"content": "版本发布窗口临近,请完成最后一轮检查并同步相关同学。",
"img": "https://xxx.png",
"button": [
{ "type": "normal", "text": "稍后处理", "actionType": "url", "action": "https://example.com/release-notes" }
]
}
}

ext 字段​

字段类型必填说明
titlestring否卡片标题,为空则不显示
contentstring否卡片正文,为空则不显示
imgstring否卡片配图地址,为空则不显示
buttonobject[]否卡片按钮数组,为空则不显示

ext 中没有任何必填业务字段:只带 title 的卡片、只带按钮的卡片都是合法的。 但 ext 本身必须存在,且必须是 JSON 对象——传数组或字符串会返回 card ext is invalid。

文字长度限制​

title 与 content 是卡片的文字描述,按 Unicode 字符计数,缺失或显式为 null 视为未配置, 不参与校验。两个字段在卡片上的展示位不同,各自使用独立上限:

字段上限超限时返回
title64 个字符card title is too long
content2000 个字符card content is too long
情况结果
单个字段不超过各自上限通过
单个字段超过各自上限拒绝,返回 card title is too long 或 card content is too long
字段取值为数字、对象、数组等非字符串拒绝,返回 invalid card title 或 invalid card content

上限按单个字段计算,title 与 content 不会合并计数。校验失败会导致整批请求被拒绝, 同一批次的所有用户都不会收到消息。

离线推送文案​

接收方 App 离线或处于后台时,卡片消息通过系统通知栏下发,展示文案按以下顺序取值:

顺序取值说明
1ext.title标题非空(去空白后)时优先作为通知文案
2ext.content标题为空时使用卡片正文
3你有一条新消息标题与正文都为空时使用兜底文案

通知文案按 80 个字符截断,ext.img、ext.button 等结构化字段不会进入通知栏。 同一会话在推送聚合窗口内累计多条消息时,通知栏改展示「你有 N 条新消息」,不再展示单条文字。

以上通知文案——包括兜底文案、聚合条数模板,以及图片、语音、视频、文件、位置、通话记录等各消息类型的 占位文案——统一在 common/constant/message.go 的「离线推送通知栏文案」常量表中定义。 文案语言当前固定为简体中文,多语言切换能力留待后续版本迭代。

按钮字段​

字段类型说明
typestring按钮样式,normal(普通按钮)或 primary(主按钮)
textstring按钮文案
actionTypestring按钮事件类型,当前仅支持 url
actionstring按钮执行事件;actionType=url 时为打开的网页地址

按钮的展示与跳转规则:

  • 卡片按钮的跳转地址统一由客户端内置浏览器打开。

  • 第一行最多展示两个按钮,其余按钮每个独占一行并撑满宽度:

    按钮数布局
    1 个该按钮撑满一整行
    2 个首行并排显示两个
    3 个及以上首行两个,其余每个独占一行

地址校验规则​

卡片中所有跳转地址必须携带 http:// 或 https:// 协议头。

以下字段会被校验,值必须是绝对 HTTP/HTTPS URL:

字段校验条件
ext.img只要非空就校验
ext.button[].action仅当该按钮的 actionType 为 url 时校验
取值结果
绝对 HTTP/HTTPS URL(如 https://example.com/a)通过
省略协议的地址(如 www.example.com)拒绝
javascript:、file:、data: 等自定义协议拒绝
空字符串通过,该字段视为未配置

校验失败会导致整批请求被拒绝,同一批次的所有用户都不会收到消息。 接口只返回统一的系统错误文案,不返回具体是哪个字段、哪个值不合法, 排障时需自行检查卡片内容。

按钮的 action 仅在该按钮的 actionType 为 url 时才会被校验;actionType 缺失或取值不为 url 时,该地址不会被校验,客户端也不会按网页跳转处理。因此链接类按钮请始终下发 actionType: "url",并携带完整协议头。

展示规则​

以下规则影响消息的最终呈现效果,发送前建议确认卡片内容:

  • title、content、img、button 任一为空时,对应区域不展示。
  • 卡片内容不会自动补全,也不会有兜底文案。
  • 卡片解析异常时,可展示字段为空。

会话列表摘要​

机器人会话列表中的摘要按以下优先级取值:

ext.title 非空 → 使用 title
ext.title 为空 → 使用 ext.content
两者都为空 → 使用统一卡片占位文案

搜索​

用户搜索时,卡片的可搜索文本仅包含 title 与 content,不包含图片、按钮文案与 URL。

通道差异​

card 类型仅在应用机器人通道(POST /open/v1/robot/messages)可用。

群机器人 Webhook 通道(POST /webhook/robot/send)支持的类型为 text、image、voice、video、file、location、custom,不包含 card。

幂等与重试机制​

项目说明
幂等标识请求头 Idempotency-Key,调用方必填
作用范围应用维度唯一,有效期为 24 小时
语义校验服务端记录请求体摘要指纹
相同报文重复提交判定为网络超时或重复重试,直接返回上次处理结果,不产生重复消息
不同报文复用同一 Key服务端拒绝请求,返回错误 IDEMPOTENCY_KEY_REUSED

重试建议:在网络超时或未获得明确响应时,必须保持完全相同的 Idempotency-Key 与请求体进行重试。

响应参数​

成功响应(HTTP 200,平台统一响应外壳):

{
"code": 200,
"msg": "",
"data": {
"requestId": "req-1710000000000000000",
"results": [
{
"index": 0,
"userId": "10001",
"status": "queued",
"sessionId": "30001",
"messageId": "1234567890123456789",
"seq": "102"
},
{
"index": 1,
"userId": "10002",
"status": "rejected",
"errorCode": "USER_NOT_ALLOWED",
"errorMessage": "user is not allowed"
}
],
"summary": {
"total": 2,
"success": 1,
"failed": 1
}
}
}

响应字段说明​

字段类型说明
codeinteger业务状态码,200 表示请求已成功接收处理
msgstring错误信息,成功时为空
data.requestIdstring对应请求头 X-Request-Id
data.resultsobject[]目标用户逐条投递结果,顺序与请求 userids 一致
data.results[].indexinteger对应请求中 userids 的下标(从 0 开始)
data.results[].userIdstring目标用户 ID
data.results[].statusstring投递状态:queued(已排队入库)、duplicate(幂等去重命中)、rejected(权限拦截)、failed(失败)
data.results[].sessionIdstring该用户与机器人的单聊会话 ID(成功时返回)
data.results[].messageIdstring消息唯一 ID(MID,成功时返回)
data.results[].seqstring该会话内严格单调递增的序列号(成功时返回)
data.results[].errorCodestring失败或拦截时的错误码(仅失败时返回)
data.results[].errorMessagestring错误详情描述(仅失败时返回)
data.summaryobject汇总统计
data.summary.totalinteger目标用户总数
data.summary.successinteger成功进入下发队列数(queued + duplicate 计入成功)
data.summary.failedinteger失败/拦截总数(rejected + failed 计入失败)

投递状态(status)取值​

  • queued:消息已被服务端接受并成功写入消息队列,即将下发给客户端。
  • duplicate:命中幂等缓存,此前已成功投递。
  • rejected:目标用户未通过权限校验(如超出可见范围、用户不存在或被禁用)。
  • failed:服务端会话创建或消息投递执行失败。

错误码说明​

鉴权与传输错误(HTTP 状态码)​

HTTP 状态码msg 错误码说明与处理建议
400INVALID_REQUESTContent-Type 不为 application/json,或缺少 X-Request-Id
401INVALID_TOKEN令牌缺失、已过期、失效,或应用已被停用/删除
403SCOPE_DENIED令牌缺少 robot.push 权限
403IP_DENIED请求来源 IP 未命中应用安全设置的 IP 白名单
503AUTH_UNAVAILABLE鉴权依赖暂时不可用

请求级业务错误(HTTP 200,code != 200)​

msg 错误码说明
missing idempotency key请求头缺少 Idempotency-Key
invalid request请求参数缺失,robotCode、userids 或 msg.mtype 为空
content is required文本消息类型的 content 为空
card ext is invalid卡片消息的 ext 缺失、不是合法 JSON 对象,或传入了字符串
card title is too long卡片 ext.title 超过 64 个字符
card content is too long卡片 ext.content 超过 2000 个字符
invalid card title卡片 ext.title 不是字符串(如传入了数字、对象或数组)
invalid card content卡片 ext.content 不是字符串(如传入了数字、对象或数组)
unsupported message typemtype 不是 text 或 card
robot unavailable机器人或应用未启用、未发布,或机器人账号异常
invalid user id at index N第 N 个用户 ID 格式不是合法的正整数字符串
duplicate user iduserids 列表中包含重复的用户 ID
IDEMPOTENCY_KEY_REUSED同一个 Idempotency-Key 绑定了不同的请求体
IDEMPOTENCY_UNAVAILABLE幂等存储不可用,请稍后重试

单项用户错误码(results[].errorCode)​

errorCode对应 status说明
USER_NOT_ALLOWEDrejected目标用户不在当前应用的可见范围内,或用户不存在
USER_UNAVAILABLErejected / failed用户状态不可用(已被禁用或删除)
SESSION_FAILEDfailed创建或查询机器人单聊会话失败
SESSION_TYPE_INVALIDfailed会话服务返回的会话类型不是机器人会话
MESSAGE_FAILEDfailed消息投递 MQ 失败

调用示例​

Shell (cURL) 完整示例​

#!/usr/bin/env bash
set -euo pipefail

# 基础配置
GATEWAY_HOST="https://im-gateway.example.com"
APP_ID="app_10001"
APP_SECRET="link_sec_xxxxxxxxxxxxxxxxxxxx"
ROBOT_CODE="robot_7f3c1a2b-0d4e-4f5a-9b6c-8d7e6f5a4b3c"

# 获取应用级 access_token
echo "==> 正在获取 access_token..."
TOKEN_RESP=$(curl -s -X POST "${GATEWAY_HOST}/auth/v1/oauth/token" \
-H "Content-Type: application/json" \
-d "{
\"client_id\": \"${APP_ID}\",
\"client_secret\": \"${APP_SECRET}\",
\"grant_type\": \"client_credentials\"
}")

ACCESS_TOKEN=$(echo "${TOKEN_RESP}" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
if [ -z "${ACCESS_TOKEN}" ]; then
echo "获取 access_token 失败: ${TOKEN_RESP}"
exit 1
fi
echo "==> 获取成功: ${ACCESS_TOKEN:0:10}..."

# 构造请求头与消息体
REQUEST_ID="req-$(date +%s%N)"
IDEMPOTENCY_KEY="idem-$(date +%s)-${RANDOM}"

# 示例:发送结构化卡片消息
REQ_BODY=$(cat <<EOF
{
"robotCode": "${ROBOT_CODE}",
"userids": ["10001", "10002"],
"msg": {
"mtype": "card",
"content": "",
"ext": {
"title": "版本发布",
"content": "版本发布窗口临近,请完成最后一轮检查并同步相关同学。",
"img": "https://xxx.png",
"button": [
{
"type": "normal",
"text": "稍后处理",
"actionType": "url",
"action": "https://example.com/release-notes"
}
]
}
}
}
EOF
)

# 发送机器人消息
echo "==> 正在发送机器人消息..."
RESP=$(curl -s -X POST "${GATEWAY_HOST}/open/v1/robot/messages" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "X-Request-Id: ${REQUEST_ID}" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d "${REQ_BODY}")

echo "==> 接口返回结果:"
echo "${RESP}"

Golang 完整示例​

package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

// 配置参数
const (
GatewayHost = "https://im-gateway.example.com"
AppID = "app_10001"
AppSecret = "link_sec_xxxxxxxxxxxxxxxxxxxx"
RobotCode = "robot_7f3c1a2b-0d4e-4f5a-9b6c-8d7e6f5a4b3c"
)

// OAuthTokenResponse 获取 Token 的响应结构
type OAuthTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Error string `json:"error,omitempty"`
}

// CardButton 卡片按钮
type CardButton struct {
Type string `json:"type"` // normal 或 primary
Text string `json:"text"`
ActionType string `json:"actionType"` // url 或 none
Action string `json:"action,omitempty"`
}

// CardExt 卡片内容结构
type CardExt struct {
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
Img string `json:"img,omitempty"`
Button []CardButton `json:"button,omitempty"`
}

// RobotMessage 消息结构
type RobotMessage struct {
MType string `json:"mtype"` // text 或 card
Content string `json:"content,omitempty"` // 文本正文或卡片摘要
Ext *CardExt `json:"ext,omitempty"` // 卡片扩展对象
}

// SendMessageRequest 发送消息请求体
type SendMessageRequest struct {
RobotCode string `json:"robotCode"`
UserIDs []string `json:"userids"`
Msg RobotMessage `json:"msg"`
}

// UserResult 单个用户的投递结果
type UserResult struct {
Index int `json:"index"`
UserID string `json:"userId"`
Status string `json:"status"` // queued, duplicate, rejected, failed
SessionID string `json:"sessionId,omitempty"`
MessageID string `json:"messageId,omitempty"`
Seq string `json:"seq,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
}

// Summary 批量汇总
type Summary struct {
Total int `json:"total"`
Success int `json:"success"`
Failed int `json:"failed"`
}

// SendMessageResponse 发送消息响应
type SendMessageResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
RequestID string `json:"requestId"`
Results []UserResult `json:"results"`
Summary Summary `json:"summary"`
} `json:"data"`
}

var httpClient = &http.Client{Timeout: 10 * time.Second}

// FetchAccessToken 获取应用级 access_token
func FetchAccessToken(ctx context.Context, host, appID, appSecret string) (string, error) {
reqBody, _ := json.Marshal(map[string]string{
"client_id": appID,
"client_secret": appSecret,
"grant_type": "client_credentials",
})

url := fmt.Sprintf("%s/auth/v1/oauth/token", host)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("token 请求失败: %w", err)
}
defer resp.Body.Close()

bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("获取 token 错误 (HTTP %d): %s", resp.StatusCode, string(bodyBytes))
}

var tokenResp OAuthTokenResponse
if err := json.Unmarshal(bodyBytes, &tokenResp); err != nil {
return "", fmt.Errorf("解析 token 响应失败: %w", err)
}
return tokenResp.AccessToken, nil
}

// SendRobotMessage 调用发送机器人消息接口
func SendRobotMessage(ctx context.Context, host, token, reqID, idemKey string, payload SendMessageRequest) (*SendMessageResponse, error) {
bodyData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("序列化请求体失败: %w", err)
}

url := fmt.Sprintf("%s/open/v1/robot/messages", host)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyData))
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Request-Id", reqID)
req.Header.Set("Idempotency-Key", idemKey)

resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送消息 HTTP 请求失败: %w", err)
}
defer resp.Body.Close()

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("服务端返回异常 (HTTP %d): %s", resp.StatusCode, string(respBytes))
}

var result SendMessageResponse
if err := json.Unmarshal(respBytes, &result); err != nil {
return nil, fmt.Errorf("解析业务响应失败: %w", err)
}
return &result, nil
}

func main() {
ctx := context.Background()

// 1. 获取 access_token
token, err := FetchAccessToken(ctx, GatewayHost, AppID, AppSecret)
if err != nil {
fmt.Printf("获取 Token 失败: %v\n", err)
return
}
fmt.Println("成功获取 access_token")

// 2. 构造消息体(以结构化卡片为例)
reqPayload := SendMessageRequest{
RobotCode: RobotCode,
UserIDs: []string{"10001", "10002"},
Msg: RobotMessage{
MType: "card",
Content: "",
Ext: &CardExt{
Title: "版本发布",
Content: "版本发布窗口临近,请完成最后一轮检查并同步相关同学。",
Img: "https://xxx.png",
Button: []CardButton{
{
Type: "normal",
Text: "稍后处理",
ActionType: "url",
Action: "https://example.com/release-notes",
},
},
},
},
}

requestID := fmt.Sprintf("req-%d", time.Now().UnixNano())
idempotencyKey := fmt.Sprintf("idem-%d", time.Now().UnixNano())

// 3. 执行单聊机器人消息推送
resp, err := SendRobotMessage(ctx, GatewayHost, token, requestID, idempotencyKey, reqPayload)
if err != nil {
fmt.Printf("推送消息失败: %v\n", err)
return
}

// 4. 打印投递结果
fmt.Printf("消息发送完成! 状态码: %d, 汇总: 总数 %d, 成功 %d, 失败 %d\n",
resp.Code, resp.Data.Summary.Total, resp.Data.Summary.Success, resp.Data.Summary.Failed)

for _, item := range resp.Data.Results {
fmt.Printf(" - 用户 %s: 状态=%s, 会话ID=%s, 消息ID=%s, 错误码=%s\n",
item.UserID, item.Status, item.SessionID, item.MessageID, item.ErrorCode)
}
}

PHP​

<?php
// 中文注释:卡片字段保持为嵌套对象,按钮动作使用纯 HTTP(S) URL。
$payload = [
'robotCode' => 'robot_xxx',
'userids' => ['10001'],
'msg' => [
'mtype' => 'card',
'content' => '',
'ext' => [
'title' => '版本发布',
'content' => '版本发布窗口临近,请完成最后一轮检查并同步相关同学。',
'img' => 'https://xxx.png',
'button' => [[
'type' => 'normal',
'text' => '稍后处理',
'actionType' => 'url',
'action' => 'https://example.com/release-notes',
]],
],
],
];
$handle = curl_init('https://{gateway_host}/open/v1/robot/messages');
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer access-token-xxx',
'Content-Type: application/json',
'X-Request-Id: req-20260920-0002',
'Idempotency-Key: idem-20260920-0002',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($handle);
curl_close($handle);

C++​

#include <curl/curl.h>

int main() {
// 中文注释:action 字段必须是纯 URL,不能把 Markdown 链接语法作为请求值。
const char* payload = R"({
"robotCode": "robot_xxx",
"userids": ["10001"],
"msg": {
"mtype": "card",
"content": "",
"ext": {
"title": "版本发布",
"content": "版本发布窗口临近,请完成最后一轮检查并同步相关同学。",
"img": "https://xxx.png",
"button": [{
"type": "normal",
"text": "稍后处理",
"actionType": "url",
"action": "https://example.com/release-notes"
}]
}
}
})";
CURL* handle = curl_easy_init();
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Authorization: Bearer access-token-xxx");
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "X-Request-Id: req-20260920-0002");
headers = curl_slist_append(headers, "Idempotency-Key: idem-20260920-0002");
curl_easy_setopt(handle, CURLOPT_URL, "https://{gateway_host}/open/v1/robot/messages");
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(handle, CURLOPT_POSTFIELDS, payload);
const CURLcode result = curl_easy_perform(handle);
curl_slist_free_all(headers);
curl_easy_cleanup(handle);
return result == CURLE_OK ? 0 : 1;
}

会话与展示规则​

机器人消息进入客户端后以机器人会话展示。应用后端只负责发送消息,不应假设客户端会话已存在或会立即打开。

展示要点​

  • 应用机器人以应用身份展示,群聊机器人以群内机器人身份展示。
  • 会话列表摘要由客户端根据消息类型生成,撤回、卡片和附件可能使用不同的摘要文案。
  • 机器人消息仍受目标用户、应用状态和可见范围约束。
  • 应用后端应使用服务端返回的消息标识和逐项投递状态做审计,不要以客户端展示作为成功依据。

相关接口​

批量发送(多个目标用户)​

本接口同时承担单聊与批量发送:userids 传入多个目标用户时,服务端按用户逐项投递并返回逐项结果。除 userids 扩展为目标用户列表外,请求头、幂等键和其他字段与单聊发送完全一致。

{ "robotCode": "robot_xxx", "userids": ["user-001", "user-002"], "msg": { "mtype": "text", "content": "批量通知" } }
  • code != 200 表示整批请求失败,不能按部分成功处理。
  • results[] 是逐项结果,必须按用户逐个处理:rejected 通常是权限或可见范围问题,failed 才按错误码决定是否重试。
  • 批量请求应按业务规模分批,避免瞬时触达过多成员。
  • 网络超时使用同一 Idempotency-Key 与请求体重试,避免重复发送。