Skip to content

Commit 815ba94

Browse files
committed
refactor(issue-1200): [feature]-dsa-后续开发规划:[通知渠道网关]-渠道基线、测试、bo
1 parent a75a0c5 commit 815ba94

4 files changed

Lines changed: 242 additions & 36 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1616
- [修复] 收紧 ntfy 结构化校验,避免 URL 编码空白 topic 被误判为有效通知端点。
1717
- [文档] 补充 Bark custom webhook 示例和 WebPush / Apprise 通知渠道评估,明确本轮不新增运行时依赖或配置入口。
1818
- [修复] 聚合报告通知按静态渠道隔离发送失败,并补充自定义 Webhook 部分成功诊断与脱敏测试。
19+
- [文档] P7 收口:`docs/notifications.md` 补齐场景化章节,并接入 `scripts/generate_notification_actions_env_table.py` 的 Actions env 对照表自动生成/校验来源,保留与 workflow 的映射一致性。
1920
- [修复] 未配置 Tushare / Longbridge 凭据时不再实例化对应可选 fetcher,避免缺失凭据的数据源进入候选集。
2021
- [修复] Longbridge 遇到连接关闭类异常后会进入冷却期,并在美股/港股实时与日线请求中临时跳过该数据源,避免请求级频繁重连。
2122
- [修复] Pytdx 股票名称查询在全部服务器不可达时会短暂冷却,并在冷却期内跳过重复探测,减少无效拨号与告警噪音。

docs/notifications.md

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,43 +35,70 @@
3535

3636
## GitHub Actions 映射
3737

38-
仓库自带 `.github/workflows/daily_analysis.yml` 只显式导入固定变量名。P0 补齐以下已存在发送链路所需的映射
38+
仓库自带 `.github/workflows/daily_analysis.yml` 只显式透传固定变量,下面表直接来源于 `analyze` step 的 `env:`。更新 workflow 后先运行
3939

40-
- `CUSTOM_WEBHOOK_BODY_TEMPLATE`
41-
- `WEBHOOK_VERIFY_SSL`
42-
- `FEISHU_WEBHOOK_SECRET`
43-
- `FEISHU_WEBHOOK_KEYWORD`
44-
- `PUSHPLUS_TOPIC`
45-
46-
P3 补齐以下通知路由映射:
47-
48-
- `NOTIFICATION_REPORT_CHANNELS`
49-
- `NOTIFICATION_ALERT_CHANNELS`
50-
- `NOTIFICATION_SYSTEM_ERROR_CHANNELS`
51-
52-
P4 补齐以下通知降噪映射:
53-
54-
- `NOTIFICATION_DEDUP_TTL_SECONDS`
55-
- `NOTIFICATION_COOLDOWN_SECONDS`
56-
- `NOTIFICATION_QUIET_HOURS`
57-
- `NOTIFICATION_TIMEZONE`
58-
- `NOTIFICATION_MIN_SEVERITY`
59-
- `NOTIFICATION_DAILY_DIGEST_ENABLED`
60-
61-
P6-A / P6-C 补齐以下 ntfy / Gotify 渠道映射:
40+
```bash
41+
python scripts/generate_notification_actions_env_table.py
42+
```
6243

63-
- `NTFY_URL`
64-
- `NTFY_TOKEN`
65-
- `GOTIFY_URL`
66-
- `GOTIFY_TOKEN`
44+
<!-- GENERATED: notifications-actions-env-table -->
45+
| 通知环境变量 | workflow 映射表达式 |
46+
| --- | --- |
47+
| `WECHAT_WEBHOOK_URL` | `${{ secrets.WECHAT_WEBHOOK_URL }}` |
48+
| `WECHAT_MSG_TYPE` | `${{ vars.WECHAT_MSG_TYPE \|\| secrets.WECHAT_MSG_TYPE \|\| 'markdown' }}` |
49+
| `FEISHU_WEBHOOK_URL` | `${{ secrets.FEISHU_WEBHOOK_URL }}` |
50+
| `FEISHU_WEBHOOK_SECRET` | `${{ secrets.FEISHU_WEBHOOK_SECRET }}` |
51+
| `FEISHU_WEBHOOK_KEYWORD` | `${{ vars.FEISHU_WEBHOOK_KEYWORD \|\| secrets.FEISHU_WEBHOOK_KEYWORD }}` |
52+
| `TELEGRAM_BOT_TOKEN` | `${{ secrets.TELEGRAM_BOT_TOKEN }}` |
53+
| `TELEGRAM_CHAT_ID` | `${{ secrets.TELEGRAM_CHAT_ID }}` |
54+
| `TELEGRAM_MESSAGE_THREAD_ID` | `${{ secrets.TELEGRAM_MESSAGE_THREAD_ID }}` |
55+
| `EMAIL_SENDER` | `${{ vars.EMAIL_SENDER \|\| secrets.EMAIL_SENDER }}` |
56+
| `EMAIL_PASSWORD` | `${{ secrets.EMAIL_PASSWORD }}` |
57+
| `EMAIL_RECEIVERS` | `${{ vars.EMAIL_RECEIVERS \|\| secrets.EMAIL_RECEIVERS }}` |
58+
| `EMAIL_SENDER_NAME` | `${{ vars.EMAIL_SENDER_NAME \|\| secrets.EMAIL_SENDER_NAME \|\| 'daily_stock_analysis股票分析助手' }}` |
59+
| `PUSHOVER_USER_KEY` | `${{ secrets.PUSHOVER_USER_KEY }}` |
60+
| `PUSHOVER_API_TOKEN` | `${{ secrets.PUSHOVER_API_TOKEN }}` |
61+
| `NTFY_URL` | `${{ secrets.NTFY_URL }}` |
62+
| `NTFY_TOKEN` | `${{ secrets.NTFY_TOKEN }}` |
63+
| `GOTIFY_URL` | `${{ secrets.GOTIFY_URL }}` |
64+
| `GOTIFY_TOKEN` | `${{ secrets.GOTIFY_TOKEN }}` |
65+
| `PUSHPLUS_TOKEN` | `${{ secrets.PUSHPLUS_TOKEN }}` |
66+
| `PUSHPLUS_TOPIC` | `${{ vars.PUSHPLUS_TOPIC \|\| secrets.PUSHPLUS_TOPIC }}` |
67+
| `CUSTOM_WEBHOOK_URLS` | `${{ secrets.CUSTOM_WEBHOOK_URLS }}` |
68+
| `CUSTOM_WEBHOOK_BEARER_TOKEN` | `${{ secrets.CUSTOM_WEBHOOK_BEARER_TOKEN }}` |
69+
| `CUSTOM_WEBHOOK_BODY_TEMPLATE` | `${{ vars.CUSTOM_WEBHOOK_BODY_TEMPLATE \|\| secrets.CUSTOM_WEBHOOK_BODY_TEMPLATE }}` |
70+
| `DISCORD_WEBHOOK_URL` | `${{ secrets.DISCORD_WEBHOOK_URL }}` |
71+
| `DISCORD_BOT_TOKEN` | `${{ secrets.DISCORD_BOT_TOKEN }}` |
72+
| `DISCORD_MAIN_CHANNEL_ID` | `${{ secrets.DISCORD_MAIN_CHANNEL_ID }}` |
73+
| `FEISHU_APP_ID` | `${{ secrets.FEISHU_APP_ID }}` |
74+
| `FEISHU_APP_SECRET` | `${{ secrets.FEISHU_APP_SECRET }}` |
75+
| `FEISHU_FOLDER_TOKEN` | `${{ secrets.FEISHU_FOLDER_TOKEN }}` |
76+
| `ASTRBOT_URL` | `${{ secrets.ASTRBOT_URL }}` |
77+
| `ASTRBOT_TOKEN` | `${{ secrets.ASTRBOT_TOKEN }}` |
78+
| `SERVERCHAN3_SENDKEY` | `${{ secrets.SERVERCHAN3_SENDKEY }}` |
79+
| `SLACK_WEBHOOK_URL` | `${{ secrets.SLACK_WEBHOOK_URL }}` |
80+
| `SLACK_BOT_TOKEN` | `${{ secrets.SLACK_BOT_TOKEN }}` |
81+
| `SLACK_CHANNEL_ID` | `${{ secrets.SLACK_CHANNEL_ID }}` |
82+
| `NOTIFICATION_REPORT_CHANNELS` | `${{ vars.NOTIFICATION_REPORT_CHANNELS \|\| secrets.NOTIFICATION_REPORT_CHANNELS }}` |
83+
| `NOTIFICATION_ALERT_CHANNELS` | `${{ vars.NOTIFICATION_ALERT_CHANNELS \|\| secrets.NOTIFICATION_ALERT_CHANNELS }}` |
84+
| `NOTIFICATION_SYSTEM_ERROR_CHANNELS` | `${{ vars.NOTIFICATION_SYSTEM_ERROR_CHANNELS \|\| secrets.NOTIFICATION_SYSTEM_ERROR_CHANNELS }}` |
85+
| `NOTIFICATION_DEDUP_TTL_SECONDS` | `${{ vars.NOTIFICATION_DEDUP_TTL_SECONDS \|\| secrets.NOTIFICATION_DEDUP_TTL_SECONDS \|\| '0' }}` |
86+
| `NOTIFICATION_COOLDOWN_SECONDS` | `${{ vars.NOTIFICATION_COOLDOWN_SECONDS \|\| secrets.NOTIFICATION_COOLDOWN_SECONDS \|\| '0' }}` |
87+
| `NOTIFICATION_QUIET_HOURS` | `${{ vars.NOTIFICATION_QUIET_HOURS \|\| secrets.NOTIFICATION_QUIET_HOURS }}` |
88+
| `NOTIFICATION_TIMEZONE` | `${{ vars.NOTIFICATION_TIMEZONE \|\| secrets.NOTIFICATION_TIMEZONE }}` |
89+
| `NOTIFICATION_MIN_SEVERITY` | `${{ vars.NOTIFICATION_MIN_SEVERITY \|\| secrets.NOTIFICATION_MIN_SEVERITY }}` |
90+
| `NOTIFICATION_DAILY_DIGEST_ENABLED` | `${{ vars.NOTIFICATION_DAILY_DIGEST_ENABLED \|\| secrets.NOTIFICATION_DAILY_DIGEST_ENABLED \|\| 'false' }}` |
91+
<!-- END GENERATED: notifications-actions-env-table -->
92+
93+
脚本也可用于 CI 校验:
6794

68-
默认 workflow 仍不映射 `MARKDOWN_TO_IMAGE_CHANNELS``MERGE_EMAIL_NOTIFICATION`。它们是发送形态或聚合行为开关,不是渠道凭证;在 Actions 中自动开始读取同名 Secret/Variable 会引入额外行为变化。
95+
```bash
96+
python scripts/generate_notification_actions_env_table.py --check
97+
```
6998

7099
## CLI 诊断
71100

72-
```bash
73-
python main.py --check-notify
74-
```
101+
`python main.py --check-notify`
75102

76103
该命令只读配置,不发送通知,不写入 `.env`。它会在配置加载和日志初始化后立即执行,完成后直接退出,不再进入 Web、调度、大盘复盘或默认分析流程。
77104

@@ -230,9 +257,30 @@ Apprise 后续如要引入,应先作为可选依赖评估,而不是默认依
230257
- 发送失败应隔离在 Apprise 渠道内,不能影响已有渠道的失败隔离语义。
231258
- 如果采用 Apprise,建议先新增单独 experimental channel 或 CLI-only spike,再决定是否纳入 Web 设置页和 Actions env。
232259

233-
## 场景占位
260+
`MARKDOWN_TO_IMAGE_CHANNELS``MERGE_EMAIL_NOTIFICATION` 仍属于运行时行为开关,不在默认 `daily_analysis.yml` env 中显式映射。
261+
262+
## 场景化接入
263+
264+
### 本地配置
265+
266+
-`.env` 为主,优先级低于运行时环境变量。
267+
-`python main.py --check-notify` 做首次一次性诊断,不会发送通知,不会写 `.env`
268+
- 通知设置页可触发单渠道测试;`CUSTOM_WEBHOOK_URLS` 支持多 URL,结果返回每 URL 级别 attempts 聚合。
269+
270+
### Docker
271+
272+
- Docker 场景沿用同一配置加载路径,可直接复用 `.env`
273+
- 建议通过 compose / 启动脚本将通知相关 env 全量注入,避免依赖默认值导致高级选项静默生效。
274+
- 若需 `WEBHOOK_VERIFY_SSL=false`,请仅在可信内网链路中开启。
275+
276+
### GitHub Actions
277+
278+
- 默认 workflow 只会读取 `analyze` step 下 `env:` 的显式变量;新增通知变量后要同时修改 workflow 映射。
279+
- 路由/降噪键建议放到 Variables;敏感值建议放 Secrets。
280+
- 非映射行为键(如 `MARKDOWN_TO_IMAGE_CHANNELS`)仍不会默认生效,除非同步扩展 workflow。
281+
282+
### Desktop
234283

235-
- Local:优先使用 `.env`,可用 `python main.py --check-notify` 做本地诊断。
236-
- Docker:配置来源与本地一致,需确保容器环境变量已注入。
237-
- GitHub Actions:只会读取 workflow `env:` 中显式映射的 Secret/Variable。
238-
- Desktop:桌面端内嵌 Web 设置页可复用同一通知测试入口;测试仍只使用临时配置,不写入 `.env`
284+
- 桌面端直接复用 Web 设置页与通知测试面板,不会新增后端接口。
285+
- 本地运行和内嵌服务端行为一致,`send_to_context`、路由过滤和降噪都按同一后端配置解释。
286+
- 若需排障,优先使用 `--check-notify` 的 CLI 输出与同一 `NotificationTestPanel` 的 attempts 聚合结果核对。
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""Generate the notification env mapping table for docs from workflow metadata."""
3+
4+
from __future__ import annotations
5+
6+
from argparse import ArgumentParser
7+
from pathlib import Path
8+
from typing import Dict, Iterable, List
9+
10+
import yaml
11+
12+
ROOT_DIR = Path(__file__).resolve().parent.parent
13+
WORKFLOW_PATH = ROOT_DIR / ".github/workflows/daily_analysis.yml"
14+
DOCS_PATH = ROOT_DIR / "docs/notifications.md"
15+
16+
START_MARKER = "<!-- GENERATED: notifications-actions-env-table -->"
17+
END_MARKER = "<!-- END GENERATED: notifications-actions-env-table -->"
18+
19+
NOTIFICATION_ENV_PREFIXES = (
20+
"WECHAT_",
21+
"FEISHU_",
22+
"TELEGRAM_",
23+
"EMAIL_",
24+
"PUSHOVER_",
25+
"NTFY_",
26+
"GOTIFY_",
27+
"PUSHPLUS_",
28+
"CUSTOM_WEBHOOK_",
29+
"DISCORD_",
30+
"SLACK_",
31+
"SERVERCHAN3_",
32+
"ASTRBOT_",
33+
"NOTIFICATION_",
34+
)
35+
36+
37+
def _load_workflow_env(path: Path = WORKFLOW_PATH) -> Dict[str, str]:
38+
workflow = yaml.safe_load(path.read_text(encoding="utf-8"))
39+
steps = workflow["jobs"]["analyze"]["steps"]
40+
analyze_step = next(
41+
(step for step in steps if step.get("name") == "执行股票分析"),
42+
None,
43+
)
44+
assert analyze_step is not None, (
45+
"Expected daily_analysis.yml job analyze to include a step named "
46+
"'执行股票分析'."
47+
)
48+
return dict(analyze_step["env"])
49+
50+
51+
def _is_notification_env_key(key: str) -> bool:
52+
return key.startswith(NOTIFICATION_ENV_PREFIXES)
53+
54+
55+
def extract_notification_env(env: Dict[str, str]) -> List[tuple[str, str]]:
56+
return [(key, str(value)) for key, value in env.items() if _is_notification_env_key(key)]
57+
58+
59+
def _clean_mapping_cell(value: str) -> str:
60+
escaped = value.replace("|", "\\|")
61+
return f"`{escaped}`"
62+
63+
64+
def build_actions_env_table(
65+
notification_items: Iterable[tuple[str, str]],
66+
) -> str:
67+
rows = [
68+
"| 通知环境变量 | workflow 映射表达式 |",
69+
"| --- | --- |",
70+
]
71+
72+
for key, value in notification_items:
73+
rows.append(f"| `{key}` | {_clean_mapping_cell(value)} |")
74+
75+
return "\n".join(rows)
76+
77+
78+
def build_marked_table_block(notification_items: Iterable[tuple[str, str]]) -> str:
79+
return "\n".join(
80+
[
81+
START_MARKER,
82+
build_actions_env_table(notification_items),
83+
END_MARKER,
84+
]
85+
)
86+
87+
88+
def _extract_marked_block(text: str) -> str:
89+
start = text.index(START_MARKER)
90+
end = text.index(END_MARKER)
91+
if end < start:
92+
raise ValueError("Invalid marker order in docs.")
93+
return text[start + len(START_MARKER) : end].strip("\n")
94+
95+
96+
def sync_docs(
97+
*,
98+
docs_path: Path = DOCS_PATH,
99+
workflow_path: Path = WORKFLOW_PATH,
100+
check_only: bool = False,
101+
) -> int:
102+
workflow_env = _load_workflow_env(workflow_path)
103+
notification_items = extract_notification_env(workflow_env)
104+
expected_block = build_marked_table_block(notification_items)
105+
106+
docs_text = docs_path.read_text(encoding="utf-8")
107+
if START_MARKER not in docs_text or END_MARKER not in docs_text:
108+
raise ValueError(
109+
f"Docs file {docs_path} is missing table markers:"
110+
f"\n{START_MARKER}\n{END_MARKER}"
111+
)
112+
113+
old_block_with_markers = f"{START_MARKER}\n{_extract_marked_block(docs_text)}\n{END_MARKER}"
114+
if old_block_with_markers == expected_block:
115+
return 0
116+
117+
if check_only:
118+
return 1
119+
120+
new_text = docs_text.replace(old_block_with_markers, expected_block)
121+
docs_path.write_text(new_text, encoding="utf-8")
122+
return 0
123+
124+
125+
def _build_parser() -> ArgumentParser:
126+
parser = ArgumentParser(
127+
description="Render/同步每日分析 workflow 的通知环境变量对照表到 docs。"
128+
)
129+
parser.add_argument(
130+
"--check",
131+
action="store_true",
132+
help="检查 docs 表与 workflow 提取结果是否一致,不写文件。")
133+
parser.add_argument(
134+
"--workflow", default=str(WORKFLOW_PATH), help="daily_analysis workflow 路径。"
135+
)
136+
parser.add_argument(
137+
"--docs", default=str(DOCS_PATH), help="通知文档路径。"
138+
)
139+
return parser
140+
141+
142+
def main(argv: List[str] | None = None) -> int:
143+
args = _build_parser().parse_args(argv)
144+
return sync_docs(
145+
docs_path=Path(args.docs),
146+
workflow_path=Path(args.workflow),
147+
check_only=args.check,
148+
)
149+
150+
151+
if __name__ == "__main__":
152+
raise SystemExit(main())

tests/test_daily_analysis_workflow_notification_env.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Static checks for notification env mappings in daily_analysis.yml."""
33

44
from pathlib import Path
5+
from scripts.generate_notification_actions_env_table import sync_docs
56

67
import yaml
78

@@ -67,3 +68,7 @@ def test_daily_analysis_keeps_deferred_behavior_switches_unmapped() -> None:
6768

6869
for key in P0_EXCLUDED_BEHAVIOR_SWITCHES:
6970
assert key not in env
71+
72+
73+
def test_daily_analysis_notification_actions_env_table_sync() -> None:
74+
assert sync_docs(check_only=True) == 0

0 commit comments

Comments
 (0)