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:
@ -473,3 +473,19 @@ CREATE TABLE IF NOT EXISTS user_appearance (
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
-- ─── 设备推送 token(可选通道)── 语义与 init_sqlite.sql 里的同名表一致 ──
|
||||
CREATE TABLE IF NOT EXISTS push_tokens (
|
||||
token_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
owner_name TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL DEFAULT '',
|
||||
device_name TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (token_id)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_push_tokens_provider_token ON push_tokens (provider, token);
|
||||
CREATE INDEX IF NOT EXISTS idx_push_tokens_owner ON push_tokens (owner_name);
|
||||
|
||||
|
||||
@ -533,3 +533,31 @@ CREATE TABLE IF NOT EXISTS user_appearance (
|
||||
updated_at DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
-- ─── 设备推送 token(可选通道)──────────────────────────────────────────
|
||||
--
|
||||
-- 为什么有 provider 列:**推送是自部署后端的可选项,不是内置依赖**
|
||||
-- (2026-09-15 用户明确要求:「不能写死推送方式,因为我们是自部署后端」
|
||||
-- 「即推送密钥应当是可选项」)。一个自部署实例可能一个推送渠道都没配
|
||||
-- —— 这是常态而不是配置错误;也可能同时接华为 HMS 与别的通道。
|
||||
-- 加通道不该动 schema、不该动端点形状。
|
||||
--
|
||||
-- owner_name 是**注册者**(登录用户)。收件判据与 SSE 同源:一封新邮件推给
|
||||
-- 谁,推送就发给谁 —— 两条通道不该有两套「谁该收到」的定义。
|
||||
CREATE TABLE IF NOT EXISTS push_tokens (
|
||||
token_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
owner_name TEXT NOT NULL,
|
||||
-- 注册时客户端所在的会话:点通知要回到那条会话里的那封信
|
||||
session_id TEXT NOT NULL DEFAULT '',
|
||||
device_name TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
updated_at DATETIME DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
PRIMARY KEY (token_id)
|
||||
);
|
||||
-- 一个 token 只能属于一个注册者:换人登录是**转移**,不是并存 —— 否则上一任
|
||||
-- 用户的通知会继续推到同一台设备上(那是隐私事故,不只是脏数据)。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_push_tokens_provider_token ON push_tokens (provider, token);
|
||||
CREATE INDEX IF NOT EXISTS idx_push_tokens_owner ON push_tokens (owner_name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user