跳到主要内容

群聊机器人

群聊机器人绑定到指定群聊,外部系统通过 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,便于后续排查投递问题。