feat(通知): 加钉钉 / 飞书两种格式(含各自的加签算法)
## 两家签名算法不一样,照抄必错
| | 钉钉 | 飞书 |
|---|---|---|
| HMAC key | `secret` | `"{timestamp}\n{secret}"` |
| 被签内容 | `"{timestamp}\n{secret}"` | 空 |
| timestamp | 毫秒 | 秒 |
| 拼在哪 | URL query | JSON body |
| 结果 | Base64 再 urlencode | Base64 |
| 出错码 | `errcode 310000` | `code 19021` |
实现为 `BaseAdapter.sign_request()` 钩子(默认不动),两家各写各的;
`core/notifier.py` 的 `_post_once` 在发请求前调用它。
## 另外
- 钉钉:markdown 消息(title+text);成功码 0;默认限流 15/分(官方 20/分,**超限会被限 10 分钟**,留余量)
- 飞书:交互式卡片(header 颜色按事件级别 + markdown 元素);成功码 0;默认 60/分
- `PLANNED_FORMATS` 清空(下拉里不再有置灰项)
- **修一个泄漏**:`mask_url` 原来只打码 query 参数——**飞书的 token 在 URL 路径里**(`/hook/<token>`)
→ 接口回显会漏出去。现在同时处理路径 token(≥20 位随机串)与 Slack 的 `/services/T…/B…/X…` 三段。
## 验证(都跑过)
- **签名对拍官方示例**:钉钉逐字节一致(含 urlencode,`%2B` 级别)、飞书的 key/message 口径一致;
时间戳钉住后比对,不靠"自己跟自己一致"
- 成功码按格式:wecom/dingtalk/feishu 的 0 与 bark 的 200 分别判成功,各自的错误码判失败
- 端到端 15 项:钉钉/飞书在假接收端上真发(URL 带签名、body 带 timestamp/sign、卡片是 markdown 元素、
毫秒 vs 秒的时间戳),签名错时正确判失败(310000 / 19021),token 在回显里被打码
This commit is contained in:
+27
-7
@@ -88,8 +88,10 @@ daemon 线程。所以:
|
||||
|
||||
上限(超了**拒绝保存**,不静默截断):webhook ≤ 20 条、整体 JSON ≤ 60000 字符、URL ≤ 2048。
|
||||
|
||||
**安全**:URL 里有凭据(企微 `?key=`)→ 接口回显、发送记录、日志一律打码
|
||||
(`mask_url`/`scrub`);编辑时**留空即不修改**;`secret` 永不回显(只回"已配置")。
|
||||
**安全**:URL 里带凭据(企微 `?key=`、钉钉 `?access_token=`、飞书 `/hook/<token>`、
|
||||
Bark `/…/<key>`、Slack `/services/T…/B…/X…`)→ 接口回显、发送记录、日志一律打码
|
||||
(`mask_url` 同时处理 query 与**路径里的 token**;异常消息过 `scrub`);
|
||||
编辑时**留空即不修改**;`secret` 永不回显(只回"已配置")。
|
||||
|
||||
---
|
||||
|
||||
@@ -98,10 +100,27 @@ daemon 线程。所以:
|
||||
| 格式 | 请求体 | URL 怎么填 | 成功判定 / 关键约束 |
|
||||
|---|---|---|---|
|
||||
| `wecom` 企业微信 | `{"msgtype":"markdown","markdown":{"content":"…"}}` | 群机器人 → 复制的 Webhook 地址(含 `?key=`) | **`errcode==0`**;content **≤4096 字节**(按字节截断、不会截出半个汉字);**每机器人每分钟 20 条**,超限 `45009` |
|
||||
| `dingtalk` 钉钉 | `{"msgtype":"markdown","markdown":{"title","text"}}` | 群设置 → 智能群助手 → 添加机器人 → 自定义 → 复制的 Webhook 地址(含 `?access_token=`) | **`errcode==0`**;正文 ≤20000 字节;官方 20 条/分,**超限会被限流 10 分钟**(这里默认 15 留余量);机器人安全设置选「加签」时密钥必填 |
|
||||
| `feishu` 飞书 | `{"msg_type":"interactive","card":{header + markdown 元素[,"timestamp","sign"]}}` | 群设置 → 群机器人 → 添加 → 自定义机器人 → 复制的 Webhook 地址(`…/bot/v2/hook/<token>`) | **`code==0`**;请求体 ≤20KB;官方 100 条/分(这里默认 60);开了「签名校验」时密钥必填 |
|
||||
| `bark` iOS 推送 | `{"title","body","markdown","group","level"[,"device_key"]}` | Bark App 里复制的那串(`https://api.day.app/<key>`)**或** `https://api.day.app/push` + 设备 Key 填到「设备 Key」 | **`code==200`**(注意和企业微信不一样!);走 APNs,正文按 2048 字节截断;`markdown` 传富文本、`body` 传纯文本兜底 |
|
||||
| `json` 通用 / Slack | 由 `body_template` 决定 | 你自己的接收端;Slack 填它的 Incoming Webhook URL | HTTP 200(响应体里有 `code`/`errcode` 时必须为 0);模板保存前**干跑校验**;`secret` 会作为 `X-Webhook-Secret` 头发出 |
|
||||
| `bark` iOS 推送 | `{"title","body","markdown","group","level"[,"device_key"]}` | Bark App 里复制的那串(`https://api.day.app/<key>`)**或** `https://api.day.app/push` + 设备 Key 填到「设备 Key」 | **`code==200`**(注意和企业微信不一样!);走 APNs,正文按 2048 字节截断;`markdown` 字段传富文本、`body` 传纯文本兜底 |
|
||||
|
||||
> 成功码**按格式**判定(企微 0、Bark 200),写在适配器的 `ok_codes` 上——加新格式时别忘了一起定。
|
||||
> 成功码**按格式**判定(企微/钉钉/飞书 = 0,Bark = 200),写在适配器的 `ok_codes` 上——
|
||||
> 加新格式时别忘了一起定。
|
||||
|
||||
### 加签:钉钉和飞书**算法不一样**,别互相照抄
|
||||
|
||||
| | 钉钉 | 飞书 |
|
||||
|---|---|---|
|
||||
| key(HMAC 的密钥) | `secret` | `"{timestamp}\n{secret}"` |
|
||||
| message(被签内容) | `"{timestamp}\n{secret}"` | **空** |
|
||||
| timestamp 单位 | **毫秒** | **秒** |
|
||||
| 拼在哪 | **URL query**(`×tamp=…&sign=…`) | **JSON body** 的 `timestamp`/`sign` |
|
||||
| 结果编码 | Base64 后再 **urlencode** | Base64 |
|
||||
| 出错码 | `errcode 310000 invalid signature` | `code 19021 sign match fail` |
|
||||
|
||||
实现见 `core/notifier.py` 的 `DingtalkAdapter.sign_request` / `FeishuAdapter.sign_request`;
|
||||
两家的算法都用官方示例代码对拍过(钉钉逐字节一致,含 urlencode)。
|
||||
|
||||
**字段一律渲染成 `> **字段**:值` 引用行,不用 Markdown 表格**——企业微信/钉钉的 markdown
|
||||
子集不支持表格,表格会原样吐出来。
|
||||
@@ -115,9 +134,10 @@ daemon 线程。所以:
|
||||
(`BaseAdapter.url_hint/url_help/secret_label/secret_help/limit_help`),界面按当前格式渲染、
|
||||
切格式即时更新——新增格式时把这些一起填上,别把某个平台的说明写死在页面上。
|
||||
|
||||
`dingtalk`/`feishu` 在界面上是**置灰**的:适配器留了插槽(`BaseAdapter._sign/_auth_fields/
|
||||
_byte_limit/_ok_codes`),要接的时候加一个类 + 注册进 `ADAPTERS` 即可;急用可以拿通用 JSON 手搓
|
||||
(飞书的 text 格式就是 `{"msg_type":"text","content":{"text":"{{markdown}}"}}`)。
|
||||
**再加新格式**:写一个 `BaseAdapter` 子类(`render` + 可选 `sign_request`)并把元信息
|
||||
(`byte_limit`/`ok_codes`/`limit_default`/`url_hint`/`url_help`/`secret_*`/`limit_help`)填全,
|
||||
然后注册进 `ADAPTERS` —— 界面上的下拉项与提示文案会自动跟着出来,**不用改前端**。
|
||||
`PLANNED_FORMATS` 是"还没实现、下拉里置灰"的占位表(目前为空)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user