搭建消息告警模块以及跨网推送服务,支持企微,飞书,钉钉通知
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
namespace Common.Notify
|
||||
{
|
||||
/// <summary>
|
||||
/// 飞书自建应用配置(用于"手机号 → open_id"转换)
|
||||
/// 飞书群机器人 webhook 只能发文本、无法用手机号 @ 人,必须借助自建应用凭证调通讯录接口换取 open_id。
|
||||
/// 由 Program.cs 启动时从 appsettings.json 的 Feishu 节读入;未配置时相关功能自动降级。
|
||||
/// </summary>
|
||||
public static class FeishuConfig
|
||||
{
|
||||
/// <summary>飞书自建应用 App ID(cli_ 开头)</summary>
|
||||
public static string AppId { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>飞书自建应用 App Secret</summary>
|
||||
public static string AppSecret { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>是否已配置(未配置时手机号无法转 open_id,降级为仅支持直接填 open_id / all)</summary>
|
||||
public static bool IsConfigured => !string.IsNullOrWhiteSpace(AppId) && !string.IsNullOrWhiteSpace(AppSecret);
|
||||
|
||||
/// <summary>初始化飞书应用配置</summary>
|
||||
public static void Init(string appId, string appSecret)
|
||||
{
|
||||
AppId = appId ?? string.Empty;
|
||||
AppSecret = appSecret ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Common.Notify
|
||||
{
|
||||
/// <summary>
|
||||
/// IM 机器人 Webhook 加签工具(钉钉/飞书)
|
||||
/// 供 IOT_API 直连推送与 AlertBridge 跨网转发程序共用,保证两种模式签名算法一致
|
||||
/// </summary>
|
||||
public static class ImWebhookSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// 钉钉机器人加签:sign = UrlEncode(Base64(HmacSHA256(timestamp + "\n" + secret, key=secret)))
|
||||
/// </summary>
|
||||
/// <param name="timestampMs">毫秒时间戳</param>
|
||||
/// <param name="secret">加签密钥(SEC 开头)</param>
|
||||
/// <returns>URL 编码后的签名</returns>
|
||||
public static string DingTalkSign(long timestampMs, string secret)
|
||||
{
|
||||
string stringToSign = $"{timestampMs}\n{secret}";
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
byte[] signData = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
|
||||
return Uri.EscapeDataString(Convert.ToBase64String(signData));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 飞书机器人加签:sign = Base64(HmacSHA256(空串, key=timestamp + "\n" + secret))
|
||||
/// 注意与钉钉相反:飞书以"时间戳\n密钥"为 key、对空内容签名
|
||||
/// </summary>
|
||||
/// <param name="timestampSec">秒级时间戳</param>
|
||||
/// <param name="secret">加签密钥</param>
|
||||
/// <returns>Base64 签名</returns>
|
||||
public static string FeishuSign(long timestampSec, string secret)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes($"{timestampSec}\n{secret}"));
|
||||
byte[] signData = hmac.ComputeHash(Array.Empty<byte>());
|
||||
return Convert.ToBase64String(signData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
|
||||
namespace Common.Notify
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知推送异常诊断工具
|
||||
/// HttpClient 网络异常(尤其 SSL/TLS 握手失败)的真实根因藏在 InnerException 链里,
|
||||
/// 只记录外层 ex.Message 会得到"see inner exception"这类无用信息。
|
||||
/// 本工具展开整条异常链并针对常见网络故障给出可读排查提示,供三个 Notifier 复用。
|
||||
/// </summary>
|
||||
public static class NotifyError
|
||||
{
|
||||
/// <summary>
|
||||
/// 将异常展开为"外层 -> 内层 -> ..."的可读描述,并附加常见网络故障排查提示
|
||||
/// </summary>
|
||||
public static string Describe(Exception? ex)
|
||||
{
|
||||
if (ex == null) return "未知错误";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var current = ex;
|
||||
int depth = 0;
|
||||
// 最多展开 5 层,防止极端嵌套导致信息过长
|
||||
while (current != null && depth < 5)
|
||||
{
|
||||
if (depth > 0) sb.Append(" -> ");
|
||||
sb.Append(current.Message);
|
||||
current = current.InnerException;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (IsTimeout(ex))
|
||||
{
|
||||
sb.Append("|排查:请求超时,请确认目标地址可达、Webhook 域名解析正常、无防火墙拦截出站 443");
|
||||
}
|
||||
else if (IsSslFailure(ex))
|
||||
{
|
||||
sb.Append("|排查:SSL/TLS 握手失败,常见原因为 " +
|
||||
"①服务器无法访问外网(工控内网请改用\"跨网中转(Outbox)\"推送模式,由跳板机 AlertBridge 实际发送) " +
|
||||
"②企业代理/防火墙做 SSL 拦截且其根证书未被本机信任 " +
|
||||
"③系统时间不准导致证书校验失败 " +
|
||||
"④Webhook 地址主机名或端口有误(非标准 HTTPS 端口)");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为超时(TaskCanceledException 且非用户主动取消,或 TimeoutException)
|
||||
/// </summary>
|
||||
private static bool IsTimeout(Exception ex)
|
||||
{
|
||||
for (var c = ex; c != null; c = c.InnerException)
|
||||
{
|
||||
if (c is TimeoutException) return true;
|
||||
// HttpClient 超时表现为 TaskCanceledException,内部通常带 TimeoutException
|
||||
if (c is TaskCanceledException && c.InnerException is TimeoutException) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 SSL/TLS 握手或证书校验失败
|
||||
/// </summary>
|
||||
private static bool IsSslFailure(Exception ex)
|
||||
{
|
||||
for (var c = ex; c != null; c = c.InnerException)
|
||||
{
|
||||
if (c is AuthenticationException) return true;
|
||||
var msg = c.Message;
|
||||
if (!string.IsNullOrEmpty(msg) &&
|
||||
(msg.Contains("SSL", StringComparison.OrdinalIgnoreCase) ||
|
||||
msg.Contains("TLS", StringComparison.OrdinalIgnoreCase) ||
|
||||
msg.Contains("certificate", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user