# SuperPush 消息推送 > 状态:§1 / §2 / §3 / §5 为**正式口径**(控制台与服务端 CA-F261008030542 · Android 外挂包 SDK-F042 · 2026-10-19);§2.3 iOS(SDK-F043)与 §4 SuperCLI(BL-014)仍是后置规划,交付后转正。本文是 SuperPush 的单一信源;读本文前先看[接入总览](https://www.superplatform.ai/guide.md)。文中 `{spid}` / `{env}` / `{sdkVersion}` 是占位符:控制台「推送设置 → 复制接入指南」会按当前项目 × 环境与最新 SDK 版本填好,手工阅读时自行替换。 ## 1. SuperPush 是什么 SuperPush 让你给自己 App 的用户发推送通知,并在控制台看到每台设备的送达、失败与打开情况。 - **通道**:Firebase Cloud Messaging(FCM HTTP v1)。Android 与 iOS 都经 FCM,iOS 的 APNs 由 Firebase 托管(在 Firebase 控制台配 APNs key)。平台不做 APNs 直连,也不做厂商通道。 - **凭据**:每个项目 × 环境一份 Firebase 服务账号 JSON,在控制台上传,只写不读。 - **设备**:集成了推送外挂包的 App 启动后自动把设备登记到当前项目 × 环境;不需要你自己收集和上传 token。 - **受众**:指定设备、按标签、按用户 ID、全部设备。标签与用户 ID 由 App 端通过 SDK 设置。 - **记录**:每次发送一条记录,含逐设备投递状态、错误码与打开数。打开数来自 SDK 点击时的回执(同时上报 `$push_open` 事件)。 - **入口**:控制台、`sst_` API(服务端);SuperCLI 后置。定时推送 = SuperScheduler 按 cron 调用推送 API,SuperPush 自身不做调度。 - **投递语义**:至少一次。平台滚动发布或节点故障期间,极少数设备(单条消息 ≤200 台)可能收到重复通知;取消一条正在发送的消息时已发出的通知不召回。 | 限制 | 值 | |---|---| | 每环境设备数 | 100,000 | | 单次发送受众 | 50,000 台 | | 发送频率 | 每环境 20 次 / 分钟(控制台与 API 合计) | | 设备登记频率 | 每 (项目, 客户端 IP) 60 次 / 分钟;每项目 6,000 次 / 分钟 | | 标题 / 正文 | 200 / 1000 字符 | | 整条消息(标题 + 正文 + 数据 + 平台固定字段) | 4 KiB(FCM 硬上限) | | 自定义数据 | ≤20 个键,键 `^[A-Za-z0-9_.-]{1,64}$`,值 ≤1024 字符 | | 标签 | 每设备 10 个,每个 32 字符 | | 记录保留 | 逐设备投递 7 天,发送汇总 90 天;失效(unregistered)设备 30 天后清除 | 首版不做:定时与周期发送(用 SuperScheduler)、A/B、多语言模板、图片 URL 之外的富媒体、FCM topic、APNs 直连、Web Push。 ## 2. 客户端接入方式 ### 2.1 前提 1. 在 [Firebase 控制台](https://console.firebase.google.com) 建项目并添加你的 Android / iOS 应用,下载 `google-services.json`(Android)或 `GoogleService-Info.plist`(iOS)。 2. 在 Firebase「项目设置 → 服务账号」生成一个服务账号私钥 JSON,交给控制台管理员上传(见 §5.1);同时确认 Google Cloud 里已启用 **Firebase Cloud Messaging API (V1)**。App 端不需要这份 JSON。 3. App 已集成 SuperSDK,并在 `Application.onCreate` 里完成 `SuperSDK.init(context, spid, provider)`;非 prod 环境在 **init 前或紧随 init** 调用 `SuperSDK.setAPIEnvironment(env)`(SDK 把首次登记推迟到下一个主线程 tick,所以紧随 init 的 `setAPIEnvironment` 对首次登记生效;晚于此再切环境会触发一次重新登记)。 ### 2.2 Android(SDK-F042 · 已交付) 依赖与插件: ```kotlin // 根 build.gradle.kts plugins { id("com.google.gms.google-services") version "4.4.2" apply false } // app/build.gradle.kts plugins { id("com.google.gms.google-services") } dependencies { implementation("com.supersdk:supersdk-push-firebase:{sdkVersion}") } // 与 com.supersdk:supersdk 同版本 ``` 把 `google-services.json` 放到 `app/`。外挂包自带 `firebase-messaging`,不要再单独声明不同版本。外挂包只依赖 SDK 的 `api + base` 两层,不会把 Firebase 带进不接推送的宿主。 接线只需在传给 `SuperSDK.init` 的 `SuperSDKProvider` 里多实现一个 getter: ```java private final PushService pushService = new SDKPushService.Builder() .addProvider(new FirebasePushProvider(applicationContext)) .build(); @Override public PushService getPushService() { return pushService; // 默认实现返回 null = 不接推送 } ``` SDK 在 init 时解析一次这个 getter 并缓存;之后在首次取得 token、token 刷新、`setAPIEnvironment` 切换、标签 / 用户 ID 变化、权限结果返回、冷启动与回前台(通知可见性变化)时自动把设备登记到当前 spid × 环境。登记体带指纹(env + token + tags + user_id + enabled + locale + 版本),相同指纹 24 小时内不重复发送;失败不在进程内重试,到下一个触发点再试。你不需要手动获取 token 或上报。推送只在主进程初始化(多进程宿主的子进程不登记)。 可选调用(全部也可通过 `SuperSDK.getPushService()` 取到同一实例;init 前调用是安全的:`setListener` 立即生效,`setTags/setUserId` 排队到 init 后回放): ```java PushService push = SuperSDK.getPushService(); // 未接线返回 null push.requestPermission(activity, result -> { /* result.getData(): GRANTED / DENIED / PROMPT */ }); // Android 13+ 通知权限;<13 直接回当前状态 push.getPermissionStatus(); // GRANTED / DENIED / PROMPT(PROMPT = 13+ 且还没弹过系统框) push.setTags(Arrays.asList("vip", "beta")); // ≤10 个,每个 ≤32 个字符;传 null / 空列表 = 清除;越界抛 IllegalArgumentException push.setUserId("u_12345"); // ≤128 个字符;传 null / 空串 = 清除 push.setListener(new PushListener() { @Override public boolean onMessage(PushMessage m) { return false; } // 前台收到;true = 你已自行展示,SDK 不再弹系统通知 @Override public boolean onOpened(PushMessage m) { return false; } // 用户点开;true = 你整体接管,SDK 不再打开 URL }); PushStatus s = push.getStatus(); // supported / permission / registered / lastError / lastOpened(Native 读不消费) ``` **默认行为**: - **后台 / 被杀**:由系统按通知内容展示;**前台**:由外挂包自建 `supersdk_push_default` 通知渠道展示(标题 / 正文 / 大图),`onMessage` 返回 `true` 可接管不展示。无论是否展示,`onMessage` 与 H5 `push.message` 都会回调;用户关掉通知或未授权时 `getStatus().permission` 如实为 `denied` / `prompt`,设备仍登记(登记体 `enabled=false`,设备页可筛「通知已关闭」),但发送时会被排除、不计入受众。 - **点击**:经外挂包的中转 Activity 拉起 App 主 Activity;若推送是 `click.type=open_url`,SDK 先向服务端回执这次打开(≤2 秒),再按服务端返回的权威 `url` 经既有 `NavigationService.openPage` 路径打开 —— 宿主实现了 `NavigationService` 就走宿主路由,否则用 SDK 默认容器页。栈序 `主 Activity → 容器页`,返回键回首页。宿主要整体接管就在 `onOpened` 返回 `true`;只想换「怎么开页面」就实现 `NavigationService`,不用碰推送代码。回执不可达(离线 / 弱网 / 服务端异常)时**只拉起主 Activity、不打开 URL**,但 `onOpened`、`push.opened`、`$push_open` 照常执行,载荷带 `verified=false`(见下)。 - **`verified` 字段**:`PushMessage`(`onOpened` 参数 · `isVerified()` / `getOpenedAt()`)、H5 `push.opened` 载荷与 `getStatus().lastOpened`(Native `PushStatus.getLastOpened()` 读取不消费;JSB 一次性)都带 `verified`。`true` = `url / clickType` 来自服务端回执(权威);`false` = 回执不可达的降级路径,`url` 是通知 intent 的原值、未经服务端核验。**宿主若在 `onOpened` 里自行打开 `url`,请先检查 `message.isVerified()`**,否则同机恶意 App 伪造的点击 intent 可能让你打开任意页面。 - **已有自己的 `FirebaseMessagingService`**:宿主 `build.gradle` 先自行声明同版本 `implementation("com.google.firebase:firebase-messaging:25.1.3")`(外挂包以 runtime 依赖携带它,不进宿主编译 classpath),Service 不得声明 `android:process`(SDK 只在主进程 install);再在 manifest 用 `tools:node="remove"` 去掉外挂包的 `com.supersdk.push.firebase.SuperFirebaseMessagingService`,并在你的 Service 里转发: ```java @Override public void onNewToken(String token) { FirebasePushProvider.handleNewToken(token); } @Override public void onMessageReceived(RemoteMessage m) { FirebasePushProvider.handleMessage(m); } ``` 中转 Activity 不需要你处理,保留即可。 - **加了依赖但 `getPushService()` 返回 null(未接线)**:外挂包的 Service 与中转 Activity 静默 no-op —— 不登记、不路由、不上报,点击只透传拉起主 Activity;Firebase 自身行为(取 token、系统展示)不受 SDK 干预。H5 `getStatus()` 返回 `{ supported: false }`,其余调用回 `UNSUPPORTED`。 **安全边界**:`user_id` 与 `tags` 由 App 端自述,服务端不做身份校验;**按用户 ID 定向不是身份认证**,不要用它投递敏感内容(账户变动、验证码等)。推送 `data` 会原样出现在设备上,不要放密钥。 ### 2.3 iOS(SDK-F043 · 规划中) ```swift // Package.swift .package(url: "https://github.com/supersdks/ios-sdk-push-firebase.git", from: "{sdkVersion}") ``` 在 `SuperSDKProvider` 里实现 `getPushService()` 返回 `SDKPushService(providers: [FirebasePushProvider()])`,`GoogleService-Info.plist` 放进 App target,并在 Firebase 控制台上传 APNs key。权限申请走 `requestPermission`,其余 API 与 Android 对称。**交付前,iOS 对共享 JSB `SuperSDK.push.*` 一律返回 `NOT_SUPPORTED`**(当前 SDK 已内置该 stub,不会报「方法不存在」)。 ### 2.4 H5(JSSDK ≥ 2.4.0) ```js const s = await SuperSDK.push.getStatus(); // { supported, permission: 'granted'|'denied'|'prompt', registered, lastError?, lastOpened? } await SuperSDK.push.requestPermission(); // → { permission };Android 13+ 弹系统框,用户不操作最多等 120 秒 await SuperSDK.push.setTags(['vip']); // [] 或 null = 清除;越界由 Native 判定 → INVALID_ARGUMENT await SuperSDK.push.setUserId('u_12345'); // '' 或 null = 清除 const offMessage = SuperSDK.push.onMessage((m) => { /* 前台收到:m.messageId / title / body / imageUrl / url / clickType / data */ }); const offOpened = SuperSDK.push.onOpened((m) => { /* 用户点开:m.url / m.messageId / m.verified */ }); offMessage(); offOpened(); // 返回值即 unsubscribe ``` - `registered` 的语义是「存在上一次成功登记且其后没有失败」,不是「当前快照已登记」:`setTags` / 切环境后到新登记成功前的瞬态仍为 `true`。 - `lastError` 是闭集 `TOKEN_UNAVAILABLE / NOT_FOUND / INVALID_REQUEST / RATE_LIMITED / NETWORK_ERROR / UNAVAILABLE` 之一,登记成功后清空;`TOKEN_UNAVAILABLE` = Firebase 未配置或取不到 token,`NOT_FOUND` = spid / 环境未在控制台注册。 - **`lastOpened` 是一次性的**:`{ messageId, url?, clickType, openedAt, verified }`,任一 WebView 任一次 `getStatus()` 取走后即清空。它是给「用户点开通知时 WebView 还没加载、`push.opened` 事件没人接」的兜底 —— 页面加载后先调一次 `getStatus()` 读 `lastOpened`,之后用 `onOpened` 监听;两者按 `messageId` 去重,**不要轮询 `getStatus()`**(会把别的页面该拿到的 `lastOpened` 抢先消费掉)。 - `push.message` / `push.opened` 只投递给**已订阅的存活 WebView**;只能经 `SuperSDK.push.onMessage/onOpened` 订阅,新运行时 `bridge.on('push.message')` 不投递。 - 浏览器模式下 `getStatus()` 返回 `{ supported: false }`,其余方法 reject `NOT_SUPPORTED`;宿主未接线时端内同样 `supported: false`,其余回 `UNSUPPORTED`;Native 版本太老时 reject `SDK_TOO_OLD`。iOS 当前一律 `NOT_SUPPORTED`(§2.3)。 ### 2.5 自检 1. 启动 App,`adb logcat -s ss-SuperPush`,应看到 `register ok device_id=… env=… token=…(末 6 位)`(所有推送日志共用 `SuperPush` TAG,`SLogger` 加 `ss-` 前缀;日志不含完整 token、user_id 与 tags 值)。 2. 控制台「推送 → 设备」切到对应环境,出现该设备,token 尾号与日志一致。 3. 「发送推送」选中该设备发一条:手机收到(前台由外挂包展示、后台 / 被杀由系统展示),「推送记录」显示 completed、delivered=1。 4. 点击通知后记录详情「已打开」+1(点击时同步回执,数秒内可见),并打开通知里指定的页面。 常见错误: - `SENDER_ID_MISMATCH`(发送侧)= App 内的 `google-services.json` 与控制台上传的服务账号不是同一个 Firebase 项目。 - `UNREGISTERED`(发送侧)= token 已失效(卸载、清数据、换 token),App 重启后自动重新登记。 - 设备不出现 = `getPushService()` 返回了 null(看 logcat 有没有 `register` 行),或 spid / 环境与控制台不一致(`getStatus().lastError == NOT_FOUND`)。 - `lastError == TOKEN_UNAVAILABLE` = `google-services.json` 缺失 / 包名不匹配 / `google-services` 插件未应用,Firebase 取不到 token;补齐后下次冷启动自愈。 - **切环境期间 token 刷新**:设备从 prod 切到 staging 后,若 FCM 恰好刷新了 token,prod 侧保留的仍是旧 token,下次向 prod 该设备发送会把它标为 `unregistered`;切回 prod 后 SDK 自动重新登记恢复。这是预期现象,不是接入问题。 - 按用户 ID 定向发出去但「不该收到的人收到了」= `user_id` 由 App 端自述、未经验证(§2.2 安全边界),请在业务侧保证同一设备登出时调用 `setUserId(null)`。 ### 2.6 SDK 自动调用的公开接口(register / opened) 外挂包替你调用,这里只为排查与自研客户端留契约。两条都是 `POST`、JSON、≤16 KiB、经 `https://api.supersdk.com`: | 接口 | 作用 | 请求体 | 成功 `data` | |---|---|---|---| | `/superapi/v2/push/register` | 登记 / 更新当前设备(以本次请求体为准,缺省的 `user_id` / `tags` 视为清除) | `spid`(必填 `^[a-z0-9]{8}$`)、`env`(缺省 `prod`)、`device_id`(8..64)、`token`(32..4096,`[A-Za-z0-9_:-]`)、`platform`(`android` / `ios`)、`app_version` / `sdk_version`(1..32)、`locale?`、`user_id?`(1..128)、`tags?`(≤10 × ≤32)、`enabled`(通知当前是否对用户可见) | `{device_id, status:"active"}` | | `/superapi/v2/push/opened` | 点击回执:把这条消息在该设备的投递行记为已打开 | `spid`、`env?`、`device_id`、`message_id`(`^pm_[0-9a-f]{12}$`) | 命中:`{click_type, url?}`(服务端权威值,SDK 据此决定是否打开 URL;`open_app` 不带 `url`);重复回执幂等(打开数只计一次,仍回同样的 `data`)。未命中(消息不存在、不属于该项目 / 环境 / 设备,或该设备没有可打开的投递记录)回 HTTP 200 + `NOT_FOUND`(与 `spid` / `env` 不存在同一信封)、无副作用,SDK 只拉起 App、不回调、不上报。逐设备投递明细保留 7 天,超过 7 天的点击按未命中处理,只拉起 App | 公开面错误码(HTTP + 信封 `code`): | 情形 | HTTP | `code` | SDK 侧表现 | |---|---|---|---| | 字段校验失败 / 缺必填 / 非 JSON | 200 | `INVALID_REQUEST` | 不自动重试(实现或配置错误) | | 缺 `spid` | 200 | `SPID_REQUIRED` | 归一 `UNAVAILABLE` | | `spid` / `env` 不存在或语法非法;opened 未命中投递记录 | 200 | `NOT_FOUND` | register:下一求值点再试;opened:只拉起 App,不回调、不上报 | | 设备配额满(新设备) | 200 | `PUSH_DEVICE_QUOTA_EXCEEDED` | 归一 `UNAVAILABLE`;既有设备更新不受影响 | | 限流(60/min per (spid, IP) · 6000/min per spid · 平台限流存储不可用时也拒绝) | 429 | `TOO_MANY_REQUESTS` | 归一 `RATE_LIMITED`,24 小时内会补登记 | | 推送模块关闭 | 503 | `PUSH_DISABLED` | 归一 `UNAVAILABLE` | `user_id` 与 `tags` 由客户端自报、平台不校验身份:按用户 ID 定向只是「App 自称是谁」,不是身份认证。 ## 3. 服务端使用方式 业务后端用控制台签发的 `sst_` API Token 调推送 API,不经过 App。权限等于签发者在组织内的角色,发送需要 admin。一个 Token 绑定组织而不是项目,所以请求体必须带 `spid`。 ```http POST https://console.superplatform.ai/api/console/machine/push/messages Authorization: Bearer sst_... X-Super-API-ENV: {env} Idempotency-Key: Content-Type: application/json { "spid": "{spid}", "title": "双十一预热:全场 8 折", "body": "11 月 1 日 0 点开抢", "image_url": "https://cdn.example.com/promo/1111-banner.png", "click": { "type": "open_url", "url": "https://demo.example.com/promo/1111" }, "data": { "campaign": "1111" }, "audience": { "type": "tag", "tags": ["vip"] } } ``` - `spid`:目标项目(必填);`X-Super-API-ENV`:目标环境(必填,缺省不回落 prod)。 - `audience.type`:`device`(`device_ids[]` ≤1000)、`tag`(`tags[]` ≤10,命中任一)、`user`(`user_ids[]` ≤1000,按设备自报的 user_id 匹配,不是身份认证)、`all`。 - `click.type`:`open_app` 或 `open_url`(SDK 容器打开该 URL)。`data` 是扁平的字符串键值,不得使用保留键 `from` / `notification` / `message_type` / `collapse_key` / `google.*` / `gcm.*` / `spid` / `env` / `message_id` / `url` / `click_type`。 - 响应:`{"code":"SUCCESS","data":{"message_id":"pm_...","audience_estimate":1596}}`。发送是异步的,进度看 `GET /api/console/machine/push/messages/{message_id}`(同 Token + `X-Super-API-ENV`,返回状态、六项计数与时间戳)或控制台记录页。 - **幂等键(两种来源,二选一必填)**:`Idempotency-Key`(1..255 字节,建议 uuid),或由 SuperScheduler 每次执行自动注入的 `X-SuperSDK-Execution-Id`(同一次执行的多次重试共享同一个值)。同键同内容 24 小时内重复提交返回同一条记录、不会重发;同键不同内容返回 409 `PUSH_IDEMPOTENCY_CONFLICT`;两种头都缺返回 422 `INVALID_REQUEST`。 - **HTTP 状态**:这条机器面接口对业务拒绝回**非 2xx**(与控制台 / CLI 面的「业务码一律 200」不同),这样 Scheduler 或你的 HTTP 客户端不解析响应体也能知道失败。响应体仍是统一信封 `{code, message, data, extra}`。 | 业务码 | 含义 | 机器面 HTTP | 控制台 / `cli_` 面 HTTP | 可重试 | |---|---|---|---|---| | `SUCCESS` | 已受理 | 200 | 200 | — | | `PUSH_NOT_CONFIGURED` | 该环境未上传凭据 | 422 | 200 | 否 | | `PUSH_CREDENTIAL_INVALID` | 凭据已失效(设置页红灯,需重新上传或「测试连接」) | 422 | 200 | 否 | | `PUSH_AUDIENCE_EMPTY` | 受众为 0(无匹配、全部通知关闭或已失效) | 422 | 200 | 否 | | `PUSH_AUDIENCE_TOO_LARGE` | 受众 > 50,000 | 422 | 200 | 否 | | `PUSH_INVALID_ARGUMENT` | 字段级错误(`extra.field` 指出字段):标题 / 正文 / URL / `data` 形状或保留键 / 整条 > 4 KiB / `device_ids` 不属本环境 | 422 | 200 | 否 | | `INVALID_REQUEST` | 缺 `X-Super-API-ENV` / 两种幂等键都缺 / 键超长 / 非法 JSON | 422 | 200 | 否 | | `PUSH_IDEMPOTENCY_CONFLICT` | 同幂等键、不同内容 | 409 | —(控制台面无幂等键) | 否 | | `PUSH_RATE_LIMITED` | 该环境 20 次 / 分钟,`extra.retry_after_secs` 给出恢复秒数 | 429 | 200 | 是 | | `NOT_FOUND` | `spid` 不属于 Token 所在组织 / `message_id` 不存在(两者不可区分) | 404 | 200 | 否 | | `FORBIDDEN` | Token 签发者不是 admin | 403 | 403 | 否 | | (无 `code`) | Token 无效 / 过期 / 不是 `sst_` | 401 | 401 | 否 | | `PUSH_DISABLED` | 平台推送模块关闭 | 503 | 503 | 是 | - **记录里的失败码**(不是响应码):FCM 原生码原样保留(`UNREGISTERED` 设备已失效、`QUOTA_EXCEEDED`、`UNAVAILABLE` / `INTERNAL` 退避重试后仍失败、`SENDER_ID_MISMATCH` 凭据与 App 的 Firebase 项目不一致、`THIRD_PARTY_AUTH_ERROR`);平台自定义两枚:`CREDENTIAL_INVALID`(整条消息因凭据失效终止,详情页链接到推送设置)与 `PUSH_INVALID_PAYLOAD`(FCM 以消息级 `INVALID_ARGUMENT` 拒绝,详情页给出字段路径)。 - **定时推送**:在 SuperScheduler 建一条 cron 任务,目标就是上面这条接口,Header 里放 `sst_` Token(信封加密保存)与 `X-Super-API-ENV`,body 带 `spid`;不用自己填幂等键,Scheduler 每次执行恒带 `X-SuperSDK-Job` 与 `X-SuperSDK-Execution-Id`,同一次执行的重试不会重复广播。**Scheduler 执行历史中的失败 = 推送被拒绝(422/409/429/503)**,响应体里的 `code` 就是原因;若执行历史是 403 / 503 而响应体不是上面的 JSON 信封(例如 HTML),说明请求被 Cloudflare 挡在平台之外,联系平台方处理,不要改 Job。 - **投递语义**:至少一次。平台滚动发布或节点故障时最多重发一个批次(≤200 台);取消正在发送的消息时已发出的通知不召回,记录里这些设备显示为 `skipped`。 ## 4. SuperCLI 使用方式(BL-014 · 后置) 薄封装上面的控制台 API,SPID 从 `super.json` 读,`--env` 必填: ```bash supercli push devices --env prod --tag vip --output json --no-input supercli push send --env prod --title "标题" --body "正文" --audience tag --tags vip,beta --url https://... --output json --no-input supercli push records --env prod --limit 20 supercli push show pm_7f3a9c2e41b0 --env prod supercli push settings --env prod # 只显示 project_id / client_email / 最近验证,不回显私钥 ``` 凭据上传只在控制台做,CLI 不提供上传服务账号的命令。 ## 5. SuperConsole 使用方式 路径:控制台 → 业务运营 → 推送。四个页面都跟随顶栏的环境选择器;平台推送模块关闭时四页统一显示「推送功能未启用」。 ### 5.1 推送设置(admin) 上传 Firebase 服务账号 JSON,平台立即做一次连接验证(换取访问令牌 + 向 FCM 发一条 `validate_only` 的测试请求)并只保存密文;页面只显示 `firebase_project_id`、`client_email`、上传人与时间、最近验证结果(成功 / 凭据无效 / FCM API 未启用 / 网络暂时不可达)。可以随时「测试连接」(成功会把失效态恢复为已配置)或「移除」;替换成另一个 Firebase 项目的凭据或移除时,该环境排队中 / 发送中的消息会被终止为 `CREDENTIAL_INVALID`,确认框会显示条数。页面上的「复制接入指南」会把 §2 与 §3 连同当前 spid、环境与 SDK 版本一起复制,可直接交给 Agent。 ### 5.2 设备 当前环境已登记的设备:device_id、平台、App / SDK 版本、token 尾号、用户 ID、标签、状态(active / unregistered)、通知是否开启、最近活跃;页头显示配额水位与「已失效 / 通知已关闭」数。可按平台、标签、用户 ID、状态、通知开关筛选,行内「发送测试推送」直接跳到发送页并选中该设备,admin 可移除设备。空列表时页面给出三步接入引导。 ### 5.3 发送推送(admin) 选受众(指定设备 / 按标签 / 按用户 ID / 全部),填标题、正文、图片、点击动作与自定义数据,右侧实时显示预计受众数、被排除的「通知已关闭 / 已失效」数、整条消息字节数与通知预览。未上传凭据时整页引导到推送设置。提交后进入记录详情。 ### 5.4 推送记录 每条发送的状态(queued / processing / completed / failed / cancelled)与总数 / 已送达 / 失败 / 无效 token / 已跳过 / 已打开,`总数 = 已送达 + 失败 + 无效 + 已跳过`;进行中的记录可取消(已发出的不召回)。详情页有错误分布(每个错误码的含义与建议)和逐设备投递表,可按 device_id 搜索、按状态筛选;逐设备明细保留 7 天。`CREDENTIAL_INVALID` 的记录会链接到推送设置。member 可查看全部页面,但不能发送、取消、上传、移除。