跳至主要内容

批量發送人與機器人會話中機器人訊息

第三方應用透過本介面,以指定內部應用機器人的身份,向一個或多個目標成員批量發送單聊訊息(文字訊息或結構化卡片訊息)。

服務端會先為每個目標成員批量建立或復用機器人單聊會話,再並發執行訊息投遞,並按目標使用者返回逐項結果。

介面資訊​

項目說明
介面名稱批量發送人與機器人會話中機器人訊息
權限標識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 與請求體重試,避免重複發送。