这篇文章用来记录一下最近折腾的另一个项目:CMCC Notify。
项目地址:
github.com/thekfjie/cmcc-notify
感谢 OpenClaw 开源,让各大死板的服务商也开放了不少好玩的接口(🙏)。这次用到的是中国移动新消息的 Channel 接口,本来是给 OpenClaw / MClaw 这类工具接入对话用的,我想拿它收服务器通知。
邮件和 Gotify 都能用,不过能多一个直接在手机上看消息的入口也不错。服务器告警、脚本运行结果、NAS 通知这些,都可以往这里发。
现在做了两种用法:已经在用 Gotify 的,可以装插件转发消息;想直接给脚本调用的,可以部署独立服务,走 HTTP API。文字、图片和文件都接了,Docker 和 AMD64 / ARM64 的构建也有。
接入和配置
接入用的是开通并授权后拿到的 Channel API Key。程序拿这个 Key 连接网关,消息就会发给对应的绑定用户,发送时不用再填手机号。
自己接协议的话,WebSocket 握手时要带 X-API-Key,连上以后再发一帧鉴权消息:
{"type":"auth","apiKey":"ak_...","version":"2.0"}
收到 auth_ok 才算鉴权通过。SDK 里也做了定时 ping、检查 pong 和断线后的退避重连,挂着收发通知时不用自己再写一套。
实际发文字的帧很简单:
{
"type": "send",
"apiKey": "ak_...",
"content": "备份完成",
"messageId": "msg_..."
}
这里不填 to 就能发给 Key 绑定的用户。只用独立版或 Gotify 插件的话,这些交给 SDK 处理,配置好 Key 就行。
项目里把每份 Key 存成一个通道,可以起名字、加备注。配置时可以直接粘贴 ak_...,也可以把整条授权短信贴进去,程序会提取里面的 Key,其余短信内容不保存。少一点手动选中复制的麻烦。
如果一条通知需要发给几个人,就把他们各自的通道放进同一个通知组。服务端会逐个发送,并分别记录结果;有的成功、有的失败时,接口会返回 207 Multi-Status,可以看到具体是哪个通道出了问题。
这些配置在独立版的 WebUI 里就能完成,页面上也能看连接状态、发消息测试。
独立服务和 Gotify 插件
独立版主要是给监控、脚本、NAS 这些调用的。
我不想在每个脚本里都填一遍底层的 Channel API Key,以及为了保障我们的不太重要的安全性,所以单独做了“通知应用”。比如监控系统用一个应用,备份脚本用另一个,各自拿自己的 Token,发送到预先配好的通道或通知组。
创建应用时会显示一次 cn_app_... Token,服务端只保存它的 SHA-256 哈希。应用可以单独停用、轮换 Token,也可以设置每分钟的请求上限。
调用就是一个普通的 HTTP 请求:
curl -X POST https://notify.example.com/v1/notify \
-H "Authorization: Bearer <APPLICATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"title": "服务告警",
"message": "磁盘使用率超过 90%"
}'
接收目标在管理页面里配置,脚本只负责提交内容。之后要换接收通道,改应用配置就行。
WebUI 里也放了可以复制的 API 示例,以及文字、图片、文件的发送测试。刚配好时可以先在页面上试一下,再接到自己的服务里。
Gotify 插件就直接用 Gotify 自带的配置页和详情页。它会读取用户消息流,根据 Application ID 和 Priority 筛选,再转发到配置好的 CMCC 通道。
比如只转发某个监控应用的消息,或者只转发达到指定 Priority 的告警。这里的 Priority 只用来筛选消息,手机端不会因此多出什么高优先级样式。
插件页能看到连接状态、脱敏后的 Key 和发送统计,日常检查基本够用。
图片、文件和长消息
图片和文件已经实际发通过了。发送分两步:先用 HTTP multipart 上传文件,拿到 mediaUrl,再通过 WebSocket 把媒体信息发出去。
本地文件 → HTTP 上传 → mediaUrl → WebSocket 媒体消息 → 手机
上传接口是 /upload,表单里带文件和 apiKey,成功后从返回的 data 里取媒体 URL,再把文件名、MIME 类型和大小一起放进发送帧。
外部托管的媒体 URL 也可能被网关接受,不过表现没有先上传再发送稳定。现在项目里走的是本地文件上传这条路径,自己接的时候也建议先用它跑通。
上传大小目前在客户端限制为 200 MiB。这个是项目侧的限制,不代表这个范围内所有类型的文件都已经在手机上测过,接入时可以先拿一张小图或一个小文件试一下。
测试时有个比较烦的地方:媒体帧里虽然能带说明文字,但手机端不一定会显示。
所以现在如果同时填写了文字和附件,就先发一条纯文本,再发不带正文的图片或文件。正常情况下手机上会收到两条。不过这两次发送是分开的,文字发成功以后,附件仍然可能上传或发送失败,排查时需要分别看结果。
长消息的显示也得留意。目前测试下来,换行能保留,URL 也能识别,但 Markdown 和 HTML 不会渲染成排版。给通知加一堆标题、加粗标记,手机上看到的还是那些符号,用普通文字分行就好。
正文长了以后可能会折叠成带省略号的预览,完整内容要点进平台生成的网页看。这个折叠是平台处理的,SDK 没有替你删内容、做摘要或改排版。
按目前观察到的表现,想让通知尽量直接显示,可以先控制在 220 个 Unicode 字符以内,空格和换行也算进去;总行数不超过 20 行,非空行最好不超过 15 行。
这几个数字是留了余量的写法建议,不能当成平台固定的折叠阈值,更不是接口的字数上限。超过了照样能发,SDK 会保留原文,只是手机端可能需要点开网页才能看全。
告警可以把服务名、状态和关键数值放前面,比如这样就够了:
NAS 磁盘告警
数据盘使用率:92%
剩余空间:80 GB
检查时间:09:30
需要的话再在后面加一个详情链接。日报或者长日志能发,但在手机消息里翻起来就没那么方便了,重要结果尽量别埋到最后。
音频、视频也留了协议结构,还需要继续测终端表现,目前先用文字、图片和文件。
代码拆分和插件构建
两种接入方式共用一套 Go SDK,WebSocket 鉴权、心跳、断线重连和媒体上传都放在里面。仓库分成三个模块:
cmcc-notify/
├── cmcc/ CMCC Go SDK
├── gotify-plugin/ Gotify 插件
├── server/ 独立服务与 WebUI
├── docs/ 架构与协议说明
└── go.work 本地联合开发
三个目录各自有 go.mod,本地通过 go.work 一起开发,也能在 GOWORK=off 下单独构建。独立版的 HTTP 服务、配置和 WebUI 依赖留在 server/,插件只引入需要的部分。
链路大概是这样:
flowchart TB GOTIFY["Gotify 消息"] --> PLUGIN["Gotify 插件"] APP["监控 / 脚本 / NAS"] --> API["独立服务 REST API"] WEBUI["WebUI 配置与测试"] --> API PLUGIN --> SDK["CMCC Go SDK"] API --> SDK SDK --> CMCC["中国移动新消息网关"] CMCC --> PHONE["手机"]
Gotify 的 Go Plugin 构建比较麻烦,.so 和 Gotify 版本、Go 工具链、系统、CPU 架构都有关系,下载时得选匹配的产物。
目前实际构建并加载过 Gotify 3.1.1 的 ARM64 插件。CI 里也加了插件依赖检查,避免以后改独立版的时候,把不需要的依赖带进插件。
独立版的 WebUI 直接嵌入 Go 二进制,部署不用另外放一份前端文件。三个模块分别做测试,go test -race、go vet 和 Actions 检查也接上了。
使用时留意
接口返回 202 Accepted,或者 SDK 返回 accepted: true,说明本次提交的写入步骤成功了。目前没有可靠的终端送达和已读回执,所以页面上能检查连接和提交结果,手机是否收到还得实际看一下。
媒体消息还有一个 media_processed 状态,表示网关处理了媒体,也不能拿它判断手机已经收到。自己接协议时,建议先发短文字确认手机能收到,再试图片、文件和长正文,哪一步有问题会比较好找。
通知组里各个通道也是独立发送的,一个失败不会撤回其他已经提交的消息。附件同样要按通道处理:每个通道用自己的 Key 上传,再发送各自拿到的 URL,项目里没有把某个通道上传后的链接直接复用给整个组。接入时如果遇到部分失败,可以根据接口返回的各通道结果排查。
后面继续用着看,主要还想补消息历史、应用审计和更多监控系统的接入示例。网关和手机端的展示行为也需要继续观察,有问题再改。
代码和使用说明都在仓库里。Gotify 用户看 gotify-plugin/,直接调用 API 看 server/;如果只需要发送协议,也可以单独用 cmcc/ 里的 SDK。

