默认 Recovery 中间件不能直接发钉钉,因其仅捕获 panic、打印日志并返回 500,不暴露错误上下文且不支持自定义处理;需自行实现中间件,用 defer+recover 捕获 panic,提取堆栈与请求信息,构造符合钉钉 Webhook 规范(含 access_token、msgtype、text.content)的 JSON 请求体,并通过超时控制的 HTTP 客户端异步发送。

为什么默认 Recovery 中间件不能直接发钉钉
因为 gin.Recovery() 只负责捕获 panic、打印日志并返回 500,它不暴露错误上下文,也不支持自定义处理逻辑。你想在 panic 发生时触发钉钉 Webhook,就必须替换掉原生中间件,自己实现 panic 捕获 + 错误序列化 + HTTP 请求发送。
如何写一个能发钉钉的 Recovery 中间件
核心是用 recover() 拦住 panic,提取堆栈和请求信息,再用 http.Post() 或 resty.Client 推送消息到钉钉机器人地址。注意三点:
- 必须在 defer 中调用
recover(),且要放在 handler 执行前(即 middleware 的最外层 defer) - 钉钉 Webhook 地址需带
access_token=xxx查询参数,否则 400 - 请求体必须是 JSON,且含
msgtype: "text"或"markdown",text.content字段不能为空
示例片段:
诊断并恢复通过 SSH 隧道连接的 OpenClaw 节点。用于解决配对必需错误、隧道冲突、远程端点错误以及 SSH 目标配置错误等问题。
func DingTalkRecovery(webhookURL string) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
stack := debug.Stack()
reqInfo := fmt.Sprintf("[%s] %s %s", c.Request.Method, c.Request.URL.Path, c.ClientIP())
msg := fmt.Sprintf("❌ Panic in Gin:\n%s\nRequest: %s\nStack:\n%s", err, reqInfo, stack)
_ = sendToDingTalk(webhookURL, msg)
}
}()
c.Next()
}
}
sendToDingTalk 函数怎么避免超时或失败静默
钉钉接口不是强依赖,不能让报警失败阻塞主流程,也不能因网络问题拖慢响应。建议:
- 使用带超时的
http.Client,Timeout设为 2–3 秒即可 - 忽略
sendToDingTalk的返回值(或仅打本地日志),不要panic或return - Webhook URL 建议从环境变量读取,避免硬编码;可加简单校验,比如匹配
https://oapi.dingtalk.com/robot/send? - 若需更高可靠性,可把报警消息投递到本地队列(如 channel + goroutine 异步发),但 Gin 中间件生命周期短,慎用长生命周期 goroutine
实际部署时最容易漏掉的配置点
钉钉机器人必须开启「自定义关键词」或「加签」,否则消息会被拦截——这是 90% 的人卡住的地方。如果你没在消息里包含「报警」「错误」这类关键词,又没开加签,sendToDingTalk 会返回 400 或 403,但你根本看不到,因为中间件里没记录失败日志。
解决办法:在 sendToDingTalk 里加一行 log.Printf("[dingtalk] failed: %v", err),或者用 zap 记录 error level 日志。别只盯着钉钉群有没有收到,先确认请求是否发出、状态码是多少。

















