群聊機器人
群聊機器人綁定到指定群聊,外部系統透過 Webhook 即可向該群發送機器人訊息,適合監控告警、CI/CD 通知和業務系統提醒。 群聊機器人與應用機器人是兩條獨立通道,憑證、鑑權方式和訊息類型不能混用。
與應用機器人對比
| 對比項 | 群聊機器人 | 應用機器人 |
|---|---|---|
| 訊息落點 | 指定群聊 | 與成員的機器人單聊 |
| 鑑權方式 | Webhook access_token + HMAC-SHA256 簽名 | 應用級 access_token + 權限標識 |
| 憑證來源 | 在群聊中新增機器人後產生 | 應用設定中的機器人設定 |
| 支援的訊息類型 | text、image、voice、video、file、location、custom | text、card |
| 目標控制 | 由機器人綁定的群聊決定 | 由目標成員列表和應用可見範圍決定 |
| 是否需要發布應用 | 不需要 | 需要應用已發布並啟用 |
不要把群聊機器人的 access_token 用於應用級 API,也不要把應用機器人的 robotCode 用於群聊 Webhook。
介面資訊
| 項目 | 說明 |
|---|---|
| 介面名稱 | 群聊機器人發送訊息 |
| 請求方式 | POST |
| 請求位址 | https://{gateway_host}/webhook/robot/send |
| 請求格式 | application/json |
| 鑑權方式 | Query 參數 access_token、timestamp、sign |
| 成功響應 | 返回機器人訊息 msgId |
介面不使用應用級 access_token,也不要求應用發布。請求通常返回 HTTP 200,呼叫方必須繼續根據響應體中的 code 判斷業務是否成功。
接入步驟
1. 建立群聊機器人
在目標群聊中新增機器人。建立成功後儲存以下憑證:
| 憑證 | 用途 | 保管要求 |
|---|---|---|
access_token | 放在 Webhook URL 的 Query 中,標識機器人和目標群聊 | 按密鑰管理,不能公開或寫入前端 |
secret_key | 參與 HMAC-SHA256 簽名校驗 | 只保存在呼叫方服務端,不能提交到公開倉庫 |
Webhook URL 的基礎格式如下:
https://{gateway_host}/webhook/robot/send?access_token={access_token}
2. 構造請求參數
Query 參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
access_token | string | 是 | 群聊機器人的 Webhook 令牌 |
timestamp | int64 | 是 | 目前 Unix 時間戳,單位為毫秒 |
sign | string | 是 | 對簽名原文執行 HMAC-SHA256 後進行 Base64 編碼的結果 |
JSON 請求體
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
mtype | string | 是 | 訊息類型,見訊息類型 |
content | string | 是 | 訊息內容,最多 65536 個字元 |
HTTP 原始請求體最大為 1 MiB,Content-Length 和 chunked 傳輸都受此限制。content 是 JSON 字串欄位;不要把應用機器人的 card 物件直接作為群聊機器人請求體發送。
文字訊息範例:
{
"mtype": "text",
"content": "構建 #1024 已完成"
}
3. 計算簽名
簽名原文和計算方式固定如下:
stringToSign = timestamp + "\n" + secret_key
sign = Base64(HmacSHA256(stringToSign, secret_key))
計算時必須遵守以下規則:
timestamp使用毫秒級 Unix 時間戳,並以十進位字串參與計算。- 時間戳和
secret_key之間只能有一個換行符\n。 - 使用
secret_key作為 HMAC-SHA256 的密鑰。 - 對 HMAC 二進位結果做標準 Base64 編碼,再對
sign做 URL 編碼後放入 Query。 - 服務端只接受目前時間前後 5 分鐘內的簽名。
- 同一個簽名在有效期內只能使用一次;重試必須重新產生
timestamp和sign。
4. 發送請求
完整請求格式如下:
POST https://{gateway_host}/webhook/robot/send?access_token={access_token}×tamp={timestamp}&sign={url_encoded_sign}
Content-Type: application/json
{"mtype":"text","content":"構建 #1024 已完成"}
5. 處理響應
成功響應:
{
"code": 200,
"data": {
"msgId": "message-id-xxx"
},
"msg": ""
}
失敗響應:
{
"code": 500,
"data": null,
"msg": "invalid signature"
}
請求方應同時檢查網路錯誤、HTTP 狀態和響應體 code。對於 request too frequent, use a new timestamp and retry,不要復用原來的簽名,應使用新的毫秒時間戳重新計算。
訊息類型
群聊機器人支援以下訊息類型:
mtype | 說明 |
|---|---|
text | 純文字訊息 |
image | 圖片訊息 |
voice | 語音訊息 |
video | 視訊訊息 |
file | 檔案訊息 |
location | 位置訊息 |
custom | 自定義訊息 |
card 結構化卡片只適用於應用機器人,不適用於群聊機器人 Webhook。不同訊息類型的 content 由訊息服務按對應類型解析,發送前應確保內容格式與 mtype 相符。
四語言呼叫範例
以下範例都使用相同的 Query 鑑權、毫秒時間戳和文字請求體。範例中的令牌、密鑰和閘道位址均為佔位值,生產環境請從服務端密鑰設定中讀取。
Shell
#!/usr/bin/env bash
set -euo pipefail
GATEWAY_HOST="https://{gateway_host}"
WEBHOOK_TOKEN="webhook-token-xxx"
WEBHOOK_SECRET="webhook-secret-xxx"
TIMESTAMP=$(( $(date +%s) * 1000 ))
# 繁體中文註解:簽名只在服務端產生,不能把 secret_key 下發到瀏覽器或行動端。
STRING_TO_SIGN="$TIMESTAMP\n$WEBHOOK_SECRET"
SIGNATURE=$(printf '%b' "$STRING_TO_SIGN" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary \
| openssl base64 -A)
# 繁體中文註解:Query 中的 Base64 簽名必須 URL 編碼,避免 +、/ 和 = 被錯誤解析。
TOKEN_URLENCODED=$(printf '%s' "$WEBHOOK_TOKEN" | sed 's/%/%25/g; s/+/%2B/g; s|/|%2F|g; s/=/%3D/g')
SIGNATURE_URLENCODED=$(printf '%s' "$SIGNATURE" | sed 's/%/%25/g; s/+/%2B/g; s|/|%2F|g; s/=/%3D/g')
curl -sS -X POST "$GATEWAY_HOST/webhook/robot/send?access_token=$TOKEN_URLENCODED×tamp=$TIMESTAMP&sign=$SIGNATURE_URLENCODED" \
-H 'Content-Type: application/json' \
-d '{"mtype":"text","content":"構建 #1024 已完成"}'
PHP
<?php
$gatewayHost = 'https://{gateway_host}';
$accessToken = 'webhook-token-xxx';
$secretKey = 'webhook-secret-xxx';
$timestamp = (int) floor(microtime(true) * 1000);
// 繁體中文註解:時間戳與密鑰之間必須只有一個換行符,且簽名密鑰只在服務端使用。
$stringToSign = $timestamp . "\n" . $secretKey;
$sign = base64_encode(hash_hmac('sha256', $stringToSign, $secretKey, true));
$query = http_build_query([
'access_token' => $accessToken,
'timestamp' => $timestamp,
'sign' => $sign,
]);
$body = json_encode([
'mtype' => 'text',
'content' => '構建 #1024 已完成',
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$handle = curl_init($gatewayHost . '/webhook/robot/send?' . $query);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($handle);
if ($response === false) {
throw new RuntimeException(curl_error($handle));
}
curl_close($handle);
// 繁體中文註解:生產環境應解析 code,並記錄 msgId 或錯誤資訊。
echo $response;
Golang
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
func main() {
gatewayHost := "https://{gateway_host}"
accessToken := "webhook-token-xxx"
secretKey := "webhook-secret-xxx"
timestamp := time.Now().UnixMilli()
// 繁體中文註解:簽名原文必須使用毫秒時間戳、一個換行符和 secret_key。
stringToSign := strconv.FormatInt(timestamp, 10) + "\n" + secretKey
mac := hmac.New(sha256.New, []byte(secretKey))
_, _ = mac.Write([]byte(stringToSign))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
query := url.Values{}
query.Set("access_token", accessToken)
query.Set("timestamp", strconv.FormatInt(timestamp, 10))
query.Set("sign", sign)
body := bytes.NewBufferString(`{"mtype":"text","content":"構建 #1024 已完成"}`)
request, err := http.NewRequest(http.MethodPost, gatewayHost+"/webhook/robot/send?"+query.Encode(), body)
if err != nil {
panic(err)
}
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
data, _ := io.ReadAll(response.Body)
fmt.Println(string(data))
}
C++
#include <curl/curl.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <chrono>
#include <sstream>
#include <string>
std::string base64Encode(const unsigned char* data, unsigned int length) {
std::string output(4 * ((length + 2) / 3), '\\0');
const int encodedLength = EVP_EncodeBlock(
reinterpret_cast<unsigned char*>(output.data()), data, length);
output.resize(encodedLength);
return output;
}
std::string signRequest(long long timestamp, const std::string& secretKey) {
// 繁體中文註解:HMAC 輸入必須是 timestamp + 換行符 + secret_key。
const std::string stringToSign = std::to_string(timestamp) + "\n" + secretKey;
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLength = 0;
HMAC(EVP_sha256(), secretKey.data(), static_cast<int>(secretKey.size()),
reinterpret_cast<const unsigned char*>(stringToSign.data()),
stringToSign.size(), digest, &digestLength);
return base64Encode(digest, digestLength);
}
int main() {
const std::string gatewayHost = "https://{gateway_host}";
const std::string accessToken = "webhook-token-xxx";
const std::string secretKey = "webhook-secret-xxx";
const auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
const std::string sign = signRequest(timestamp, secretKey);
CURL* handle = curl_easy_init();
if (handle == nullptr) {
return 1;
}
// 繁體中文註解:對 Query 中的令牌和簽名做 URL 編碼,避免特殊字元破壞請求。
char* escapedToken = curl_easy_escape(handle, accessToken.c_str(), 0);
char* escapedSign = curl_easy_escape(handle, sign.c_str(), 0);
std::ostringstream url;
url << gatewayHost << "/webhook/robot/send?access_token=" << escapedToken
<< "×tamp=" << timestamp << "&sign=" << escapedSign;
curl_free(escapedToken);
curl_free(escapedSign);
const std::string body = R"({"mtype":"text","content":"構建 #1024 已完成"})";
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(handle, CURLOPT_URL, url.str().c_str());
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(handle, CURLOPT_POST, 1L);
curl_easy_setopt(handle, CURLOPT_POSTFIELDS, body.c_str());
const CURLcode result = curl_easy_perform(handle);
curl_slist_free_all(headers);
curl_easy_cleanup(handle);
return result == CURLE_OK ? 0 : 1;
}
錯誤碼說明
介面失敗時通常返回 HTTP 200 和 code=500 的統一響應體。以下 msg 文案用於定位問題:
msg | 原因 | 處理建議 |
|---|---|---|
access_token is required | 缺少 access_token | 在 Query 中添加機器人令牌 |
timestamp is required | 缺少 timestamp | 添加毫秒級時間戳 |
invalid timestamp | 時間戳不是合法整數 | 使用十進位 int64 毫秒時間戳 |
sign is required | 缺少 sign | 計算簽名後放入 Query |
robot not found | 令牌對應的機器人不存在 | 檢查令牌,確認機器人仍在群內 |
robot secret key not configured | 機器人沒有設定簽名密鑰 | 在機器人設定中設定或重新產生密鑰 |
signature expired | 時間戳與服務端相差超過 5 分鐘 | 使用目前時間重新計算簽名 |
invalid signature | 簽名原文、密鑰或編碼不正確 | 檢查換行符、HMAC 密鑰和 Base64 編碼 |
request too frequent, use a new timestamp and retry | 同一簽名已被消費 | 使用新的毫秒時間戳重新計算並重試 |
robot push disabled | 機器人訊息推送已關閉 | 在機器人設定中開啟訊息推送 |
session not found | 機器人關聯的群聊不存在 | 檢查機器人綁定的群聊 |
unsupported message type | mtype 不在允許列表 | 使用支援的訊息類型 |
invalid request body | JSON 格式或必填欄位錯誤 | 檢查 mtype、content 和 JSON 格式 |
request body too large | 原始請求體超過 1 MiB | 缩小請求體後重試 |
安全與重試建議
access_token和secret_key只保存在呼叫方服務端,不要放在前端、行動端、公開倉庫或日誌中。- 生產環境必須使用 HTTPS,避免令牌和訊息內容在網路中被竊聽或竄改。
- 不要把完整 Webhook URL 放入截圖、工單或監控日誌;如已洩露,應立即移除機器人或輪換憑證。
- 重試前重新取得目前毫秒時間戳並重新計算
sign,不要復用已經發送過的簽名。 - 訊息發送成功後儲存響應中的
data.msgId,便於後續排查投遞問題。