feat(push): 可选、配置式、多厂商的推送通道(HMS 为首个实现)
用户要求:推送密钥必须是可选项(自部署后端不能写死推送方式),且要支持 多厂商配置式接入 —— 每个用户各自部署服务器、自己选厂商、自己配凭证。 所以落地成: · internal/push:通道抽象 + 工厂表(RegisterType),加厂商不改配置层与端点形状; HMS 只是第一个实现(internal/push/hms.go) · 配置在 PUSH_CONFIG(默认 <AGENTMAIL_DATA_DIR>/push.json),一项一个厂商, 凭证走文件(app_secret_file / files.*,建议 600);环境变量只是可选覆盖 · 没配 = 整条推送路径连一次查库都不发生(shouldDispatch 早退); 单项配错(未知类型/密钥读不到/enabled:false)只跳过那一条,不影响启动 · push_tokens 表带 provider 维度 + 三个 /me/devices/push-token 端点; 没配推送时端点照存并回 enabled:false(登记成功 != 服务端开了推送) · notify.Recipients 末尾异步挂钩:收件人名单直接用 SSE 那份 seen(两条通道 共用同一份"谁该收到"的判据);失败只记日志,绝不拖住收信 HMS 的形状是拿真凭证打线上接口问出来的(v1 + message.token[] + testMessage; payload/target 形状 v1 不认、v2 要服务账号 JWT)。未上架应用必须 test_message=true, 单批 ≤10 token(MaxTokensPerRequest 声明)、每日 1000 条兜底(项目级额度)。 实测:App ID + App Secret 能换到 access_token(3600s);形状被线上服务接受。 判据:repo 6 条 + push 12 条 + handler 3 组,全部做过**变异验证** —— 过程中抓出两条假判据(异步分发与 t.Cleanup 赛跑而假绿;密钥文件优先级没被覆盖) 并补掉。Go 全量测试与 go vet 干净。 ★ 未验:端到端真机送达(需要真机 token + 客户端按 com.jianf.agentmail 重编并签名, 签名指纹还要在 AGC 登记)—— 从未真正发出过一条能到达设备的推送。 详见 docs/HMS-PUSH-PLAN.md 的「实现状态」一节。
This commit is contained in:
@ -224,3 +224,106 @@ push.onMessage((data) => {
|
||||
| **P4** | 测试 + 边界(token 刷新失败、推送频率、多设备) | 0.5 天 |
|
||||
|
||||
总计 Gateway 侧 **1.5 天**,可与多账号方案并行推进。
|
||||
|
||||
---
|
||||
|
||||
# 实现状态(2026-09-15)
|
||||
|
||||
本节是**交付后的事实记录**:哪些验过、哪些没验过、还有哪些前提条件。
|
||||
(本文件上半部分写于 09-07,是设计构想;实现按下面的口径做了修正。)
|
||||
|
||||
## 一、设计口径的变化:从「HMS 专用」改成「多厂商配置式」
|
||||
|
||||
用户 2026-09-15 的两条要求把设计定死了:
|
||||
|
||||
1. 「推送密钥应当是可选项」「不能写死推送方式,因为我们是自部署后端」
|
||||
2. 「我们要支持多厂商配置式接入统一推送服务,推送密钥(文件)应当是配置项,
|
||||
用户可为不同的推送服务配置对应的凭证,因为我们的设计中是每个用户各自部署服务器」
|
||||
|
||||
落地形状(`internal/push`):
|
||||
|
||||
| 关注点 | 实现 |
|
||||
|---|---|
|
||||
| 可选 | 没配任何通道 → 整条推送路径连一次查库都不发生(`shouldDispatch` 早退) |
|
||||
| 多厂商 | `Notifier` 接口 + `RegisterType()` 工厂表;一个实例可同时接多个厂商 |
|
||||
| 配置式 | `PUSH_CONFIG`(默认 `<AGENTMAIL_DATA_DIR>/push.json`),providers 是一张表 |
|
||||
| 凭证是配置项 | `app_secret_file` / `files.*` 指向密钥文件;内联 `app_secret` 仅是便利 |
|
||||
| 单项配错不致命 | 未知类型 / 密钥读不到 / `enabled:false` → 只跳过那一条,网关照常启动 |
|
||||
| provider 维度 | 表 `push_tokens` 与三个端点都带 `provider`,客户端按名字登记 |
|
||||
|
||||
端点(都需要登录,与 `/me/*` 同一套鉴权):
|
||||
|
||||
```
|
||||
POST /api/v1/me/devices/push-token {provider, token, device_name?, session_id?}
|
||||
DELETE /api/v1/me/devices/push-token {provider, token}
|
||||
GET /api/v1/me/devices/push-token → {enabled, providers[], tokens[{token_tail…}]}
|
||||
```
|
||||
|
||||
没配推送时 POST 仍返回 200 并**照存**,响应里 `enabled:false`:
|
||||
客户端据此知道「登记成功了,但服务端现在没开推送」,而不是把收不到通知当成登记失败。
|
||||
|
||||
## 二、HMS 的实际 API 形状(探针实测,不是照文档抄的)
|
||||
|
||||
拿真凭证对线上服务打了三种形状:
|
||||
|
||||
| 试法 | 服务端回答 | 结论 |
|
||||
|---|---|---|
|
||||
| `POST /v1/{appId}/messages:send` + `message.token[]`/`notification` + `testMessage` | `{"code":"80300007","msg":"All the tokens are invalid"}` | ✅ 形状被接受(只是假 token 无效) |
|
||||
| 同端点 + `payload`/`target` | `{"code":"80300010","msg":"token count should within 1 and 1,000"}` | ❌ v1 不认这种形状 |
|
||||
| `POST /v2/{appId}/messages:send` + `payload`/`target` | `{"code":"80200001","msg":"Authentication Error"}` | ❌ v2 要服务账号 JWT(未实现) |
|
||||
|
||||
认证:`POST https://oauth-login.cloud.huawei.com/oauth2/v3/token`(client_credentials),
|
||||
实测换到 access_token(有效期 3600s)✓。服务端缓存并提前 5 分钟刷新。
|
||||
|
||||
**未上架应用必须 `test_message: true`**(用户提供的信息):不开的话未上架应用限到约
|
||||
2 条/天/设备;测试消息额度是**项目级 1000 条/天**,单次最多 10 个 token
|
||||
(10 由 `MaxTokensPerRequest()` 声明,分批由 `push.dispatch` 执行)。应用上架后改
|
||||
`test_message: false`。
|
||||
|
||||
## 三、验过的 / 没验过的(不夸大)
|
||||
|
||||
**验过的:**
|
||||
|
||||
- 凭证真的能认证:App ID + App Secret → access_token(线上,3600s)✓
|
||||
- 推送请求的形状被线上服务接受(上表第一行)✓
|
||||
- 「没配通道 = 零开销」:把 `db.DB` 置 nil(任何查库都会 panic)后调用 `NotifyNewMail`
|
||||
仍不碰库 ✓,且判据做了**变异验证**(去掉早退立刻红)
|
||||
- 「单项配错不致命」「密钥文件优先」「enabled:false 生效」「环境变量只是覆盖」
|
||||
等配置语义 ✓(同样做了变异验证 —— 过程中抓出两条**假判据**并补掉)
|
||||
- 「没配推送时端点照常可用」:handler 层判据 ✓
|
||||
- Go 全量测试 ✓;`go vet` 干净 ✓
|
||||
|
||||
**没验过的(必须说清楚):**
|
||||
|
||||
- ★ **端到端真机送达从未验证过**:需要 (a) 真机产出的 push token,(b) 客户端按新包名
|
||||
重编**并签名**,(c) 该签名指纹在 AGC 登记过。本机一个都不具备 ⇒ 从未真正发出过
|
||||
一条能到达设备的推送。上面的「形状被接受」不等于「设备收得到」。
|
||||
- 成功码 `80000000` 来自 API 约定(失败码是实测的),成功路径无法在本机验证。
|
||||
- 响应里的 `illegal_tokens` 字段(用于部分无效时清理)未实测,实现是防御式的。
|
||||
- v2 端点 / 服务账号密钥鉴权**未实现**。
|
||||
- 客户端半边(Push Kit 取 token、上报、点通知跳转)由 dsh 实施,尚未完成。
|
||||
|
||||
## 四、两个前提条件(阻碍"真的能收到")
|
||||
|
||||
1. **包名必须改**:AGC 拒绝 `com.agentmail.harmony`(保留字 `harmony`,实测),
|
||||
已按用户决定改为 **`com.jianf.agentmail`**,客户端 `AppScope/app.json5` 要同步改。
|
||||
2. **签名与指纹**:设备的包名 + 签名必须与 AGC 登记的一致。AGC 应用页有
|
||||
「SHA256证书/公钥指纹:添加公钥指纹」。之前交付的 HAP 是 **unsigned** 的 ——
|
||||
推送要真的送达,客户端需要用 AGC 里登记的证书签名(或把实际使用的签名指纹登进 AGC)。
|
||||
这一条我这边无法代做(涉及你的签名材料)。
|
||||
|
||||
## 五、一次完整接入的步骤(自部署用户视角)
|
||||
|
||||
```bash
|
||||
# 1) 在厂商后台建应用,拿到 app_id / 密钥(华为:AGC → 项目设置 → 常规 → 应用)
|
||||
# 2) 把密钥写成文件并收紧权限
|
||||
install -m 600 /dev/null /etc/agentmail/hms-app-secret
|
||||
printf '%s' '<App Secret>' > /etc/agentmail/hms-app-secret
|
||||
# 3) 配置通道(照 deploy/push.json.example 改)
|
||||
cp deploy/push.json.example /opt/agentmail/data/push.json # 或 PUSH_CONFIG 指向别处
|
||||
# 4) 重启后看一行日志确认通道启用/未启用
|
||||
systemctl restart agentmail-gateway && journalctl -u agentmail-gateway | grep '\[push\]'
|
||||
```
|
||||
|
||||
启动日志会明确说出结果:`通道已启用: hms(单批最多 10 个 token)` 或
|
||||
`未配置推送通道 —— 正常状态,SSE 仍是收信主通道`;配置有错时逐条说明跳过了哪一项、为什么。
|
||||
|
||||
Reference in New Issue
Block a user