|
|
2 هفته پیش | |
|---|---|---|
| docs | 2 هفته پیش | |
| README.md | 2 هفته پیش | |
| config.py | 2 هفته پیش | |
| config.yaml.example | 2 هفته پیش | |
| mailer.py | 2 هفته پیش | |
| main.py | 2 هفته پیش | |
| notifier.py | 2 هفته پیش | |
| probes.py | 2 هفته پیش | |
| requirements.txt | 2 هفته پیش |
阿里云百炼(DashScope)模型下线通知 + 模型可用性双模块监控。
一个常驻 loop,按 interval_minutes 反复执行两个检测,命中即"邮件 + 钉钉"双重告警。
main.py loop(每 interval_minutes 一轮)
├── 模块 A check_offline_notice 检测到下线通知 -> 转发原邮件给通知列表 + 钉钉
└── 模块 B check_model_health 模型可用性检测 -> 聚合告警邮件 + 钉钉
pip install -r requirements.txt
python main.py --once --dry-run # 只检测并打印将发送的告警,不真发
python main.py --once # 真实跑一轮
python main.py # 常驻循环
命令行参数:
| 参数 | 说明 |
|---|---|
-c, --config |
配置文件路径,默认 config.yaml |
--once |
只执行一轮后退出 |
--dry-run |
只检测并打印告警内容,不真正发送 |
--skip-mail |
跳过模块 A(只测模型) |
--skip-probe |
跳过模块 B(只测邮箱) |
--test-dingtalk |
只给钉钉发一条测试消息后退出 |
offline_notice.sender_domains —— 允许触发告警的发件人域名白名单。
api_key —— 百炼 DashScope Key,所有端点探测共用这一个。
api_key: "sk-xxx" # 百炼 Key,所有端点共用
interval_minutes: 360 # 每隔多少分钟跑一轮(360 = 6 小时)
mail:
imap_host: "imap.exmail.qq.com"
imap_port: 993
inbox_folder: "INBOX"
sent_folder: "Sent Messages" # 填错会自动在常见候选名里兜底探测
smtp_host: "smtp.exmail.qq.com"
smtp_port: 465
smtp_ssl: true
username: "cat@example.com" # 监控账号,同时是发件人
password: "xxx"
append_to_sent: true # 服务商不自动归档时,用 IMAP APPEND 补一份
notify_emails: # 告警群发列表
- "someone@example.com"
offline_notice:
keyword: "模型下线通知" # 标题或正文命中即算
sender_domains: # 发件人域名白名单(不是完整地址)
- "aliyun.com"
lookback_days: 3 # 收件箱与已发送的回溯天数
models: # 只填模型名,端点参数见 config.py 的 ENDPOINTS
chat: { enabled: true, models: ["qwen3.5-plus", "qwen3.5-flash"] }
tts: { enabled: true, voice: "longxiaochun_v2", models: ["cosyvoice-v3-flash"] }
asr: { enabled: true, models: ["fun-asr"] }
text2image: { enabled: true, probe_mode: "validate", models: ["wan2.2-t2i-flash"] }
text2video: { enabled: true, probe_mode: "validate", models: ["wanx2.1-t2v-turbo"] }
dingtalk:
enabled: false
access_token: "xxx" # 机器人 token,只填这个即可
secret: "SECxxx" # 安全设置选"加签"时填,否则留空
webhook: "" # 留空用官方地址;也可粘整条自带 token 的 webhook
at_all: false # true = @所有人
timeout_seconds: 15
at_mobiles: # 需要 @ 的成员手机号
# - "13800000000"
enabled: false 的分组整组跳过;分组下写与 ENDPOINTS 同名的字段即可覆盖默认值
(如 timeout_seconds、prompt、voice、probe_mode)。
所有字段都支持 ${ENV} 注入,例如 access_token: "${DINGTALK_DEV_NOTIFY_TOKEN}"。
固定调用 https://oapi.dingtalk.com/robot/send,access_token / timestamp / sign
作为 query 参数传入,消息类型为 markdown。
sign = base64(HMAC-SHA256(secret, "{毫秒时间戳}\n{secret}")),
由 requests 的 params 负责 URL 编码 —— 不要自己 quote,二次编码会导致验签失败。at_mobiles 里的号码会同时写进 at.atMobiles 并追加到正文末尾,
因为钉钉要求 markdown 正文里出现 @手机号 字面量,@ 才会真的生效。enabled: true 但没配 access_token(且 webhook 里也没有)时,load_config 会直接报错退出,
避免"以为配好了、其实一直静默不推送"。errcode != 0 时抛异常,errmsg 会原样打进日志(token 无效 / 验签失败 /
关键词不匹配 / 限流都能从这里看出来)。单独联调机器人,不用等整轮检测:
python main.py --test-dingtalk
判定条件(两者同时满足):
lookback_days(默认 3)天收件箱内offline_notice.sender_domains,且标题或正文包含 offline_notice.keyword(默认「模型下线通知」)阿里云用哪个具体邮箱号发通知是不确定的,写死完整地址一旦对不上就会静默漏消息—— 这是最危险的失效方式。改成域名白名单后,该域名下换任何邮箱号发都能收到, 同时又拦住了外部陌生人发含关键词的骚扰邮件。
匹配规则是「域名相等或为其子域」,不是裸 endswith:
| 发件人 | aliyun.com 是否命中 |
|---|---|
noreply@aliyun.com |
✅ 本域 |
service@mail.aliyun.com |
✅ 子域 |
evil@notaliyun.com |
❌ 裸 endswith 会误放行 |
evil@fake-aliyun.com |
❌ 同上 |
evil@aliyun.com.evil.cn |
❌ 域名前缀伪装 |
aliyun.com@qq.com |
❌ 目标域塞进本地部分 |
配置写法有容错:aliyun.com、@aliyun.com、noreply@aliyun.com、ALIYUN.COM
都会被规整成 aliyun.com;只有一个域名时也可以不写成列表。
留空则不校验发件人,启动时会打 WARNING。
命中后立即把原邮件转发给 notify_emails,并同时推钉钉。转发件标题:
【模型下线通知转发】<原邮件标题>
转发件结构:
| 部分 | 内容 |
|---|---|
text/plain |
转发说明(原发件人/时间/Message-ID)+ 原邮件正文,可直接阅读 |
message/rfc822 |
原始邮件完整存档 original.eml,保留全部头信息 |
Reply-To 设为原发件人,直接回复即可回到阿里云。
去重(本地无状态):按「这封是否已经转发过」判定,依据写在信头里。
转发时把原邮件的 Message-ID 写进转发件的两个信头:
| 信头 | 作用 |
|---|---|
References |
标准转发/回复关系头 |
X-Forwarded-Msgid |
自定义头,冗余一份,防止服务商改写 References |
每轮检测先扫「已发送」近 N 天,用
BODY.PEEK[HEADER.FIELDS (REFERENCES X-FORWARDED-MSGID)] 批量取这两个头,
解析成「已转发过的原邮件 Message-ID 集合」。收件箱里命中的通知若其 Message-ID
已在集合中,说明这封已经转发过,跳过。
标题不参与去重,所以改标题不影响去重;但不要手工删除或改写转发件的这两个信头。
发信后程序会回查「已发送」确认转发关系已入库(轮询重试 4×4s,因为服务商自动归档有延迟);
若始终查不到,则用 IMAP APPEND 补一份(mail.append_to_sent: true)。
腾讯企业邮实测为自动归档、不接受 APPEND,且会完整保留上述两个信头,靠自动归档即可去重。
另外,程序会跳过发件人等于自己账号的邮件,避免转发件进自己收件箱后自我触发。
IMAP 的 FROM 只是子串匹配,仅用来缩小 fetch 范围;取回邮件后会再做一次精确的域名归属校验。
极少数邮件没有 Message-ID,则用「发件人 + 日期 + 标题」生成一个稳定标记参与去重。
按 API 端点分类,你只需要往对应分组的 models 里填模型名,
端点地址和探测参数都在 config.py 的 ENDPOINTS 里有默认值,需要覆盖时在分组下写同名字段。
| 分组 | 告警中显示名 | 端点 | 探测方式 | 费用 |
|---|---|---|---|---|
chat |
文本对话 | /compatible-mode/v1/chat/completions |
prompt 只发 ok,max_tokens=16 |
约 30 token |
tts |
文本转语音 | wss://.../api-ws/v1/inference/ |
建连 + run-task,收到 task-started 立即断开,不发合成文本 |
无 |
asr |
语音转文本 | /api/v1/services/audio/asr/transcription |
异步提交官方示例音频,拿到 task_id 即通过,不轮询 |
极低 |
text2image |
文生图 | /api/v1/services/aigc/text2image/image-synthesis |
见下方 probe_mode |
0(validate) |
text2video |
文生视频 | /api/v1/services/aigc/video-generation/video-synthesis |
见下方 probe_mode |
0(validate) |
阿里云对这两个端点是先解析模型、再校验参数的顺序,据此可以零成本判断模型是否在线:
validate(默认,推荐):故意不传 prompt。
400 InvalidParameter: input.prompt should not be null → 判为可用400 InvalidParameter: Model not exist. → 判为不可用submit:真实提交生成任务,端到端验证,会产生生成费用(视频尤其贵)邮件标题:
【模型可用性告警】3 个模型不可用:deepseek-v4、cosyvoice-v9-flash、wan9.9-t2i-flash
邮件正文:
【模型可用性告警】
检测时间: 2026-09-01 11:10:01
探测总数: 4 失败: 3
deepseek-v4(文本对话)报错 model_not_found The model `deepseek-v4` does not exist...,完整响应 {...};
cosyvoice-v9-flash(文本转语音)报错 ModelNotFound Model not found (...)!,完整响应 {...};
wan9.9-t2i-flash(文生图)报错 InvalidParameter Model not exist.,完整响应 {...}
—— model-watchdog 自动告警
单条格式为 模型名(显示名)报错 <code> <message>,完整响应 <raw>,多条之间用 ;\n 连接。
本轮所有失败模型聚合为一条告警,完整响应 保留原始响应体(最长 1500 字符)。
全部模型正常时不发任何通知(静默)。
正常一轮(无告警):
配置加载完成: 每 3 分钟一轮 | 待测模型 文本对话×2、文本转语音×1、语音转文本×1、文生图×1、文生视频×1 | 邮箱 cat@mail.gxx12138.space
下线通知匹配条件: 发件人域名=aliyun.com 关键词=模型下线通知 回溯=3 天
========== 开始第 2026-09-01 12:13:41 轮检测 ==========
[邮件] 已发送中近 3 天的转发记录: 2 封原邮件
[邮件] 收件箱近 3 天命中下线通知: 1 封
[邮件] 其中 1 封已转发过,跳过
[模块A] 无需要转发的模型下线通知
[探测] qwen3.5-plus (文本对话) 正常, 1456 ms
...
[模块B] 探测 6 个模型,失败 0 个
========== 本轮结束,耗时 3.4s,转发下线通知 0 条,不可用模型 0 个 ==========
休眠 3 分钟,下次检测 2026-09-01 12:16:44
其中「已转发过,跳过」就是去重生效的标志。首次转发时会看到:
[邮件] 已转发下线通知给 1 人: 【模型下线通知转发】xxx
[邮件] 转发记录已确认在已发送中(服务商自动归档),去重生效
| 文件 | 职责 |
|---|---|
main.py |
主循环 + 两个检测模块的编排 |
config.py |
配置加载、${ENV} 注入、端点默认值 ENDPOINTS、校验 |
config.yaml |
用户配置(只需填 key / 频次 / 邮箱 / 模型名) |
probes.py |
各 API 端点的探测实现与成败判定 |
mailer.py |
IMAP 收件检测 + 原邮件转发 + 已发送去重 + SMTP 群发 |
notifier.py |
钉钉机器人 + dual_forward(转发下线通知)/ dual_alert(可用性告警) |
nohup python main.py -c config.yaml >> watchdog.log 2>&1 &