Skip to content

Commit 70808a9

Browse files
massif-01ZhuLinsen
authored andcommitted
docs: productize webhook body templates (ZhuLinsen#1200) (ZhuLinsen#1226)
1 parent d52daa6 commit 70808a9

9 files changed

Lines changed: 157 additions & 9 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ AGENT_SKILLS=
407407
#
408408
# CUSTOM_WEBHOOK_URLS=https://oapi.dingtalk.com/robot/send?access_token=xxx,https://hooks.slack.com/services/xxx
409409
# CUSTOM_WEBHOOK_BEARER_TOKEN= # 可选,用于需要认证的 Webhook (Header Authorization: Bearer <token>)
410-
# CUSTOM_WEBHOOK_BODY_TEMPLATE= # 可选,JSON body 模板;支持 $content_json/$content/$title_json/$title
410+
# CUSTOM_WEBHOOK_BODY_TEMPLATE= # 可选,全局 JSON body 模板,会覆盖 Bark/Slack/Discord 等自动 payload;推荐 $content_json/$title_json
411411
# WEBHOOK_VERIFY_SSL=true # 默认校验。设为 false 可支持自签名证书。警告:禁用后存在 MITM 劫持风险,仅限可信内网
412412
#
413413
# 【方式六】Pushover 配置

apps/dsa-web/src/components/settings/__tests__/SettingsField.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ describe('SettingsField', () => {
8383
expect(screen.getAllByRole('button', { name: '删除' })).toHaveLength(2);
8484
});
8585

86+
it('renders localized custom webhook body template guidance', () => {
87+
const onChange = vi.fn();
88+
89+
render(
90+
<SettingsField
91+
item={{
92+
key: 'CUSTOM_WEBHOOK_BODY_TEMPLATE',
93+
value: '',
94+
rawValueExists: false,
95+
isMasked: false,
96+
schema: {
97+
key: 'CUSTOM_WEBHOOK_BODY_TEMPLATE',
98+
category: 'notification',
99+
dataType: 'string',
100+
uiControl: 'textarea',
101+
isSensitive: false,
102+
isRequired: false,
103+
isEditable: true,
104+
options: [],
105+
validation: {},
106+
displayOrder: 52,
107+
},
108+
}}
109+
value=""
110+
onChange={onChange}
111+
/>
112+
);
113+
114+
expect(screen.getByLabelText('自定义 Webhook Body 模板')).toBeInTheDocument();
115+
expect(screen.getByText(/ BarkSlackDiscord payload /)).toBeInTheDocument();
116+
expect(screen.getByText(/ \$content \/ \$title JSON /)).toBeInTheDocument();
117+
});
118+
86119
it('opens detailed field help when help metadata is available', () => {
87120
render(
88121
<SettingsField

apps/dsa-web/src/utils/systemConfigI18n.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const fieldTitleMap: Record<string, string> = {
6262
FEISHU_APP_SECRET: '飞书应用 App Secret',
6363
DINGTALK_APP_KEY: '钉钉 App Key',
6464
DINGTALK_APP_SECRET: '钉钉 App Secret',
65+
CUSTOM_WEBHOOK_BODY_TEMPLATE: '自定义 Webhook Body 模板',
6566
PUSHPLUS_TOKEN: 'PushPlus Token',
6667
REPORT_SUMMARY_ONLY: '仅分析结果摘要',
6768
MAX_WORKERS: '最大并发线程数',
@@ -127,6 +128,7 @@ const fieldDescriptionMap: Record<string, string> = {
127128
FEISHU_APP_SECRET: '仅用于飞书应用机器人 / Stream Bot / 云文档等应用模式,不会直接开启群 Webhook 推送。',
128129
DINGTALK_APP_KEY: '钉钉应用模式 App Key。',
129130
DINGTALK_APP_SECRET: '钉钉应用模式 App Secret。',
131+
CUSTOM_WEBHOOK_BODY_TEMPLATE: '全局 JSON body 模板,会先于 Bark、Slack、Discord 等自动 payload 生效;推荐使用 $content_json / $title_json,裸 $content / $title 不做 JSON 转义。',
130132
PUSHPLUS_TOKEN: 'PushPlus 推送令牌。',
131133
REPORT_SUMMARY_ONLY: '仅推送分析结果摘要,不包含个股详情。多股时适合快速浏览。',
132134
MAX_WORKERS: '异步任务队列最大并发数。配置保存后,队列空闲时会自动应用;繁忙时延后生效。',

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1818
- [修复] 修正 LLM 渠道测试中 `Your request was blocked` 等服务商或网关拦截错误被误报为网络异常的问题。
1919
- [chore] 清理仓库根目录:移除误入库的 `.codex``review.md` 跟踪记录,将 smoke 测试入口迁移到 `scripts/`、环境检查脚本迁移为 `scripts/check_env.py`,并将 LiteLLM YAML 示例迁移到 `docs/examples/`
2020
- [新功能] Web 设置页新增通知渠道一键测试,支持临时配置、耗时与脱敏 attempts 展示。
21+
- [改进] 产品化自定义 Webhook Body 模板说明,明确全局模板优先级、JSON 转义建议和 Bark / NapCat 适配示例。
2122
- [新功能] 系统设置页新增配置项帮助入口与多语言帮助文案基础设施,首批覆盖自选股、LLM 主模型、LLM 渠道、飞书 Webhook 与 WebUI 监听地址。
2223
- [改进] 设置项帮助窗口支持键盘焦点限制、Esc 关闭和关闭后焦点恢复,并移除短描述重复 hover tooltip。
2324
- [文档] 新增设置页配置帮助维护说明,明确帮助元数据字段、首批覆盖范围、事实源和多语言文案同步规则。

docs/full-guide.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,13 +743,25 @@ EMAIL_GROUP_2=user2@example.com
743743

744744
设置 `CUSTOM_WEBHOOK_URLS`,多个用逗号分隔。
745745

746-
如需适配 AstrBot、NapCat 或自建服务的特殊 body,可设置 `CUSTOM_WEBHOOK_BODY_TEMPLATE`。该值必须渲染为 JSON object,推荐使用 `$content_json` 避免换行和引号破坏 JSON:
746+
如需适配 AstrBot、NapCat 或自建服务的特殊 body,可设置 `CUSTOM_WEBHOOK_BODY_TEMPLATE`。这是全局模板,会先于 Bark、Slack、Discord 等 URL 自动识别 payload 生效;如果渲染后不是 JSON object,系统会回退默认 payload。推荐使用 `$content_json` / `$title_json` 避免换行和引号破坏 JSON:
747747

748748
```env
749749
CUSTOM_WEBHOOK_BODY_TEMPLATE={"msg_type":"text","content":$content_json}
750750
```
751751

752-
可用占位符:`$content_json`、`$content`、`$title_json`、`$title`。
752+
可用占位符:`$content_json`、`$content`、`$title_json`、`$title`。其中 `$content` / `$title` 是裸字符串,不做 JSON 转义;正文含双引号或换行时可能触发 fallback。
753+
754+
Bark 使用全局模板时需显式写出 Bark body:
755+
756+
```env
757+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"title":$title_json,"body":$content_json,"group":"stock"}
758+
```
759+
760+
NapCat / OneBot 示例需按实际 endpoint、`user_id` 或 `group_id` 调整:
761+
762+
```env
763+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"user_id":123456,"message":$content_json}
764+
```
753765

754766
### Discord
755767

docs/full-guide_EN.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,14 +643,31 @@ Supports any POST JSON Webhook, including:
643643
Set `CUSTOM_WEBHOOK_URLS`, separate multiple with commas.
644644

645645
If AstrBot, NapCat, or a self-hosted service requires a custom request body, set
646-
`CUSTOM_WEBHOOK_BODY_TEMPLATE`. The rendered value must be a JSON object. Prefer
647-
`$content_json` so newlines and quotes stay valid JSON:
646+
`CUSTOM_WEBHOOK_BODY_TEMPLATE`. This is a global template and is rendered before
647+
URL auto-detected payloads such as Bark, Slack, or Discord. If the rendered value
648+
is not a JSON object, DSA falls back to the default payload. Prefer
649+
`$content_json` / `$title_json` so newlines and quotes stay valid JSON:
648650

649651
```env
650652
CUSTOM_WEBHOOK_BODY_TEMPLATE={"msg_type":"text","content":$content_json}
651653
```
652654

653655
Available placeholders: `$content_json`, `$content`, `$title_json`, `$title`.
656+
Raw `$content` / `$title` are not JSON-escaped, so quotes or newlines can make
657+
the template invalid and trigger fallback.
658+
659+
When using Bark with a global template, include the Bark body explicitly:
660+
661+
```env
662+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"title":$title_json,"body":$content_json,"group":"stock"}
663+
```
664+
665+
NapCat / OneBot examples must be adjusted for your actual endpoint, `user_id`,
666+
or `group_id`:
667+
668+
```env
669+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"user_id":123456,"message":$content_json}
670+
```
654671

655672
### Discord
656673

docs/notifications.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 通知能力基线
22

3-
本文档记录通知能力 P0/P1 基线:渠道、配置 key、GitHub Actions 映射、Web 设置元数据、CLI 诊断口径和 Web 一键测试。P0 只做基线与只读诊断;P1 增加 Web 单渠道真实测试,不包含渠道路由、降噪、模板产品化等后续 Phase 能力
3+
本文档记录通知能力 P0-P2 基线:渠道、配置 key、GitHub Actions 映射、Web 设置元数据、CLI 诊断口径、Web 一键测试和自定义 Webhook Body 模板语义。P0 只做基线与只读诊断;P1 增加 Web 单渠道真实测试;P2 只产品化现有 Body 模板,不包含渠道路由、降噪、per-URL 模板或新增一等渠道
44

55
## 渠道基线
66

@@ -25,7 +25,7 @@
2525

2626
- Minimal key:足以启用一个通知渠道的最小配置。
2727
- Advanced key:只影响认证、安全、格式、线程、群组、证书校验或展示行为,不能单独启用渠道。
28-
- P0 不新增路由、降噪或发送策略语义;相关配置如未来引入,应先更新本文档、`.env.example`、Web 元数据与回归测试。
28+
- P0-P2 不新增路由、降噪或发送策略语义;相关配置如未来引入,应先更新本文档、`.env.example`、Web 元数据与回归测试。
2929

3030
## GitHub Actions 映射
3131

@@ -60,6 +60,46 @@ Web 设置页的“通知渠道”分类提供单渠道测试入口。测试会
6060
- 返回结果会脱敏 token、secret、password、Bearer、完整 webhook query 和疑似 path token。
6161
- 配置缺失或发送失败返回 `success=false`,不会影响已保存配置和默认分析流程。
6262

63+
## 自定义 Webhook Body 模板
64+
65+
`CUSTOM_WEBHOOK_BODY_TEMPLATE` 是自定义 Webhook 的全局 JSON body 模板。配置后,它会先于 URL 自动识别生效,因此会覆盖 Bark、Slack、Discord、钉钉等自动 payload。未配置时仍使用原有 URL 自动识别;渲染后不是合法 JSON object 时会记录错误并回退默认 payload,不中断主通知流程。
66+
67+
可用占位符:
68+
69+
- `$content_json`:JSON 转义后的通知正文,推荐默认使用。
70+
- `$title_json`:JSON 转义后的通知标题,推荐默认使用。
71+
- `$content` / `$title`:原始字符串,不做 JSON 转义。正文含双引号、反斜杠或换行时可能导致 JSON 无效并触发 fallback。
72+
73+
通用 webhook 示例:
74+
75+
```env
76+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"title":$title_json,"content":$content_json}
77+
```
78+
79+
Bark 通过 custom webhook 使用时,默认会按 `api.day.app` 自动生成 `title` / `body` / `group`。如果配置全局模板,需要自己写出 Bark body:
80+
81+
```env
82+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"title":$title_json,"body":$content_json,"group":"stock"}
83+
```
84+
85+
AstrBot 已是一等通知渠道,优先使用 `ASTRBOT_URL` 和可选的 `ASTRBOT_TOKEN`。只有需要把 AstrBot 兼容端点放入 `CUSTOM_WEBHOOK_URLS` 时,才使用 custom webhook 模板,例如:
86+
87+
```env
88+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"content":$content_json}
89+
```
90+
91+
NapCat / OneBot HTTP API 需要按实际 endpoint 和目标类型调整。下面只是常见 body 形态示例,`user_id``group_id`、URL 路径和鉴权方式都应以你的 NapCat 配置为准:
92+
93+
```env
94+
# 私聊:CUSTOM_WEBHOOK_URLS=http://127.0.0.1:3000/send_private_msg
95+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"user_id":123456,"message":$content_json}
96+
```
97+
98+
```env
99+
# 群聊:CUSTOM_WEBHOOK_URLS=http://127.0.0.1:3000/send_group_msg
100+
CUSTOM_WEBHOOK_BODY_TEMPLATE={"group_id":123456789,"message":$content_json}
101+
```
102+
63103
## 场景占位
64104

65105
- Local:优先使用 `.env`,可用 `python main.py --check-notify` 做本地诊断。

src/core/config_registry.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -904,8 +904,10 @@
904904
"CUSTOM_WEBHOOK_BODY_TEMPLATE": {
905905
"title": "Custom Webhook Body Template",
906906
"description": (
907-
"Optional JSON body template for custom webhooks. Supports $content_json, "
908-
"$content, $title_json, and $title placeholders."
907+
"Optional global JSON body template for custom webhooks. It is rendered before "
908+
"URL auto-detected payloads such as Bark, Slack, or Discord, and must render to a "
909+
"JSON object. Prefer $content_json and $title_json; raw $content and $title are "
910+
"not JSON-escaped and can make the template invalid."
909911
),
910912
"category": "notification",
911913
"data_type": "string",

tests/test_notification_sender.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,47 @@ def test_bark_payload_truncates_long_content(self):
437437
self.assertEqual(len(payload["body"]), 4000)
438438
self.assertEqual(payload["body"], "x" * 4000)
439439

440+
def test_custom_body_template_overrides_bark_auto_payload(self):
441+
cfg = _config(
442+
custom_webhook_body_template=(
443+
'{"title":$title_json,"body":$content_json,"sound":"bell"}'
444+
),
445+
)
446+
sender = CustomWebhookSender(cfg)
447+
448+
payload = sender._build_custom_webhook_payload("https://api.day.app/key", "hello")
449+
450+
self.assertEqual(
451+
payload,
452+
{
453+
"title": "股票分析报告",
454+
"body": "hello",
455+
"sound": "bell",
456+
},
457+
)
458+
self.assertNotIn("group", payload)
459+
460+
def test_custom_body_template_json_placeholders_escape_content(self):
461+
cfg = _config(
462+
custom_webhook_body_template=(
463+
'{"title":$title_json,"content":$content_json}'
464+
),
465+
)
466+
sender = CustomWebhookSender(cfg)
467+
468+
payload = sender._build_custom_webhook_payload(
469+
"https://example.com/webhook",
470+
'line 1\nline "2"',
471+
)
472+
473+
self.assertEqual(
474+
payload,
475+
{
476+
"title": "股票分析报告",
477+
"content": 'line 1\nline "2"',
478+
},
479+
)
480+
440481
@mock.patch("src.notification_sender.custom_webhook_sender.requests.post")
441482
def test_send_uses_custom_body_template(self, mock_post):
442483
mock_post.return_value = _response(200)

0 commit comments

Comments
 (0)