跳至主要内容

群聊機器人

群聊機器人綁定到指定群聊,外部系統透過 Webhook 即可向該群發送機器人訊息,適合監控告警、CI/CD 通知和業務系統提醒。 群聊機器人與應用機器人是兩條獨立通道,憑證、鑑權方式和訊息類型不能混用。

與應用機器人對比​

對比項群聊機器人應用機器人
訊息落點指定群聊與成員的機器人單聊
鑑權方式Webhook access_token + HMAC-SHA256 簽名應用級 access_token + 權限標識
憑證來源在群聊中新增機器人後產生應用設定中的機器人設定
支援的訊息類型text、image、voice、video、file、location、customtext、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_tokenstring是群聊機器人的 Webhook 令牌
timestampint64是目前 Unix 時間戳,單位為毫秒
signstring是對簽名原文執行 HMAC-SHA256 後進行 Base64 編碼的結果

JSON 請求體​

參數類型必填說明
mtypestring是訊息類型,見訊息類型
contentstring是訊息內容,最多 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))

計算時必須遵守以下規則:

  1. timestamp 使用毫秒級 Unix 時間戳,並以十進位字串參與計算。
  2. 時間戳和 secret_key 之間只能有一個換行符 \n。
  3. 使用 secret_key 作為 HMAC-SHA256 的密鑰。
  4. 對 HMAC 二進位結果做標準 Base64 編碼,再對 sign 做 URL 編碼後放入 Query。
  5. 服務端只接受目前時間前後 5 分鐘內的簽名。
  6. 同一個簽名在有效期內只能使用一次;重試必須重新產生 timestamp 和 sign。

4. 發送請求​

完整請求格式如下:

POST https://{gateway_host}/webhook/robot/send?access_token={access_token}&timestamp={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&timestamp=$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
<< "&timestamp=" << 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 typemtype 不在允許列表使用支援的訊息類型
invalid request bodyJSON 格式或必填欄位錯誤檢查 mtype、content 和 JSON 格式
request body too large原始請求體超過 1 MiB缩小請求體後重試

安全與重試建議​

  1. access_token 和 secret_key 只保存在呼叫方服務端,不要放在前端、行動端、公開倉庫或日誌中。
  2. 生產環境必須使用 HTTPS,避免令牌和訊息內容在網路中被竊聽或竄改。
  3. 不要把完整 Webhook URL 放入截圖、工單或監控日誌;如已洩露,應立即移除機器人或輪換憑證。
  4. 重試前重新取得目前毫秒時間戳並重新計算 sign,不要復用已經發送過的簽名。
  5. 訊息發送成功後儲存響應中的 data.msgId,便於後續排查投遞問題。