mcpField
A
axiom-push-notifications-diag

axiom-push-notifications-diag

github.com/charleswiltgen开发1.2k66 次安装今天更新
代码仓库

关于 axiom-push-notifications-diag

面向开发者的 iOS 推送通知故障排查技能,涵盖令牌、配置、负载与投递问题,并强调先核验权限和令牌注册。

主要功能

  1. 01核验权限与令牌
  2. 02验证 APNs 负载
  3. 03检查通知授权状态
  4. 04按决策树排查问题
  5. 05模拟器与 FCM 诊断

适用场景

  1. 01排查未收到通知
  2. 02分析开发与生产环境差异
  3. 03诊断静默或富媒体推送

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-push-notifications-diag

安装后在 Claude Code 中输入/axiom-push-notifications-diag确认已加载。

详细介绍

推送通知诊断

系统地排查推送通知故障:通知缺失、令牌注册错误、环境不匹配、静默推送节流以及服务扩展问题。

概述

核心原则:推送通知无法正常工作时,问题通常出在:

  1. 令牌/注册故障(从未注册、格式错误、已过期)——30%
  2. 权限/配置文件不匹配(缺少功能、环境错误)——25%
  3. 负载结构错误(缺少键、类型错误、JSON 无效)——15%
  4. 专注模式/中断抑制(iOS 15+ 过滤、临时授权)——15%
  5. 服务扩展故障(超时、崩溃、缺少 mutable-content)——10%
  6. 投递时机/节流(静默推送额度、APNs 合并)——5%

在排查负载或投递逻辑之前,务必先验证权限和令牌注册。

危险信号

表明存在推送特有问题的症状:

症状可能原因
完全没有通知缺少推送通知功能或配置文件
开发环境可用,生产环境不可用使用生产令牌向 APNs 沙盒发送(或反之)
在 Simulator 上令牌注册失败这是预期行为——Simulator 无法注册远程通知
通知出现但没有声音授权选项或负载中缺少 .sound
富通知显示为纯文本负载中缺少 mutable-content: 1
通知中未显示图像服务扩展静默失败——检查 serviceExtensionTimeWillExpire
静默推送未唤醒应用系统节流(约每小时 2-3 次),或应用已被用户强制退出
iOS 更新后通知停止iOS 15+ 默认启用了专注模式;检查中断级别
徽标显示数量错误发送多条通知时未显式重置徽标数量
未显示操作负载中的类别标识符与已注册类别不匹配
通知出现两次同一事件同时安排了本地通知和远程通知
FCM 在 Android 上可用,但在 iOS 上不可用Firebase Console 中缺少 APNs 身份验证密钥上传

反合理化

合理化说法为什么行不通时间成本
“昨天还能用,所以授权配置没问题”签名变更时,配置描述文件会重新生成。务必重新验证。配置描述文件丢失推送能力后,调试代码需花费 30-60 分钟
“服务端说他们的负载没问题”55% 的推送失败发生在客户端(授权配置 + 令牌)。使用 curl 独立验证。在有人检查之前,互相推诿 1-2 小时
“我跳过令牌验证,错误显然出在负载上”环境不匹配的令牌是“开发环境正常,生产环境不行”的首要原因。使用有效负载向无效令牌发送消息,调试需花费 30 分钟以上
“专注模式无关紧要,我们使用默认中断级别”默认级别(active)会被专注模式过滤。只有 time-sensitive 和 critical 能突破限制。为本可在负载层修复的问题花费数小时添加代码变通方案
“静默推送很可靠,我们用它来同步”系统会将其限制在每小时约 2-3 次,并忽略被强制退出的应用。它只是一个提示,并非保证。静默推送无法维持实时同步时,需要重做架构
“服务扩展已配置好,所以富通知应该能正常工作”扩展需要正确的 Bundle ID 后缀、负载中的 mutable-content,并且必须在 30 秒内完成。缺少这三项前提中的任意一项时,排查需花费 30 分钟以上
“FCM 会处理一切,我不需要了解 APNs”FCM 封装了 APNs。令牌类型混淆、未上传 p8 密钥,以及 swizzling 冲突都是 APNs 层面的问题。问题出在 APNs 配置,却花数小时调试 FCM
“我先在 Simulator 上测试”Simulator 无法注册远程通知。没有 APNs 令牌,就无法进行真正的推送测试。浪费一个测试周期后才发现 Simulator 的限制
“我来重写通知处理程序”80% 的推送失败是配置问题(授权配置、令牌、环境),而非代码问题。配置仍有问题,却花数小时重写正常工作的代码
“这在 iOS 17 上能用,问题肯定出在我们的代码里”每个 iOS 版本都会改变专注模式默认设置、中断过滤和临时授权行为。修复本应通过修改负载或设置完成,却在调试代码

必须先完成的步骤

调查代码之前,请运行以下诊断:

步骤 1:验证推送通知授权配置
security cms -D -i path/to/embedded.mobileprovision | grep -A1 "aps-environment"

预期输出:

  • ✅ <string>development</string> 或 <string>production</string> → 授权配置项存在
  • ❌ 没有 aps-environment 键 → 未在 Xcode 中启用推送通知功能

查找配置描述文件的方法:

# For installed app on device
find ~/Library/Developer/Xcode/DerivedData -name "embedded.mobileprovision" -newer . 2>/dev/null | head -3
步骤 2:检查令牌注册
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    print("✅ APNs token: \(token)")
    print("✅ Token length: \(token.count) chars")
}

func application(_ application: UIApplication,
                 didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("❌ Registration failed: \(error.localizedDescription)")
}

预期输出:

  • ✅ 64 个字符的十六进制令牌 → 注册成功
  • ❌ "no valid aps-environment entitlement" → 功能配置错误
  • ❌ 完全没有触发回调 → 从未调用 registerForRemoteNotifications()

重要:两个回调都必须位于 AppDelegate,而不是 SceneDelegate。SwiftUI 应用需要 @UIApplicationDelegateAdaptor。

步骤 3:使用 curl 验证负载
curl -v \
  --header "apns-topic: com.your.bundle.id" \
  --header "apns-push-type: alert" \
  --header "authorization: bearer $JWT_TOKEN" \
  --data '{"aps":{"alert":{"title":"Test","body":"Hello"}}}' \
  --http2 https://api.sandbox.push.apple.com/3/device/$DEVICE_TOKEN

预期输出:

  • ✅ HTTP/2 200 → APNs 已接受负载
  • ❌ 400 BadDeviceToken → 令牌格式错误或已过期
  • ❌ 403 ExpiredProviderToken → JWT 已超过 1 小时
  • ❌ 403 InvalidProviderToken → 密钥 ID、团队 ID 或密钥错误
  • ❌ 410 Unregistered → 应用已卸载或令牌已失效
  • ❌ 413 PayloadTooLarge → 超过 4096 字节
步骤 4:检查授权状态
let settings = await UNUserNotificationCenter.current().notificationSettings()
print("Authorization: \(settings.authorizationStatus.rawValue)")
print("Alert: \(settings.alertSetting.rawValue)")
print("Sound: \(settings.soundSetting.rawValue)")
print("Badge: \(settings.badgeSetting.rawValue)")

预期输出:

  • ✅ authorizationStatus = 2 → 已授权
  • ⚠️ authorizationStatus = 3 → 临时授权(会在通知中心静默显示)
  • ❌ authorizationStatus = 1 → 用户已拒绝
  • ❌ authorizationStatus = 0 → 尚未确定(从未请求)

决策树

决策树 1:收不到任何通知
Not receiving any notifications?
│
├─ Check Step 1 (entitlements)
│  ├─ No aps-environment key?
│  │  └─ Enable Push Notifications in Signing & Capabilities → DONE
│  └─ aps-environment present → continue
│
├─ Check Step 2 (token registration)
│  ├─ didFailToRegister called?
│  │  ├─ "no valid aps-environment" → Regenerate provisioning profile
│  │  └─ Other error → Check network, device (not Simulator)
│  ├─ Neither callback fires?
│  │  └─ Verify registerForRemoteNotifications() called after app launch
│  └─ Token received → continue
│
├─ Check Step 3 (payload delivery)
│  ├─ HTTP 200 but no notification?
│  │  └─ Check Step 4 (authorization status)
│  ├─ 400 BadDeviceToken?
│  │  └─ Token expired or wrong environment → Re-register
│  └─ 403/410 error?
│     └─ Fix auth credentials or re-register device
│
└─ Check Step 4 (user authorization)
   ├─ Status: denied?
   │  └─ User must enable in Settings → Show settings prompt
   ├─ Status: notDetermined?
   │  └─ Call requestAuthorization() → Was never requested
   └─ Status: authorized but still no notifications?
      └─ Check Focus mode, Do Not Disturb, notification grouping
决策树 2:开发环境正常,生产环境不行
Works in development, fails in production?
│
├─ APNs endpoint correct?
│  ├─ Dev: api.sandbox.push.apple.com
│  └─ Prod: api.push.apple.com
│     └─ Using sandbox endpoint with production build? → Switch endpoint
│
├─ Token environment matches?
│  ├─ Dev and production tokens are DIFFERENT
│  │  └─ Server storing dev token, sending to prod APNs? → Re-register on prod build
│  └─ Server distinguishes token environments? → Add environment flag to token storage
│
├─ Auth method correct?
│  ├─ .p8 key (token-based)?
│  │  └─ Same key works for both environments ✅
│  └─ .p12 certificate?
│     ├─ Dev cert → Only works with sandbox
│     └─ Prod cert → Only works with production
│        └─ Wrong cert for environment? → Generate correct certificate
│
└─ Using FCM?
   ├─ APNs auth key (.p8) uploaded to Firebase Console?
   │  └─ Missing? → Upload in Project Settings > Cloud Messaging
   └─ Key uploaded but wrong Team ID?
      └─ Verify Team ID matches Apple Developer account
决策树 3:静默通知无法唤醒应用
Silent push not waking app?
│
├─ Payload correct?
│  ├─ Has "content-available": 1 in aps?
│  │  └─ Missing? → Add to aps dictionary
│  ├─ Has NO "alert", "badge", or "sound" in aps?
│  │  └─ Has alert? → Not a silent push; system treats as visible notification
│  └─ Payload valid → continue
│
├─ Headers correct?
│  ├─ apns-push-type: background?
│  │  └─ Missing or wrong? → Must be "background" for silent push
│  └─ apns-priority: 5?
│     └─ Using 10? → Silent push MUST use priority 5
│
├─ Background mode enabled?
│  ├─ "Remote notifications" checked in Background Modes capability?
│  │  └─ Missing? → Enable in Signing & Capabilities
│  └─ application(_:didReceiveRemoteNotification:fetchCompletionHandler:) implemented?
│     └─ Missing? → Implement the delegate method
│
├─ App state?
│  ├─ Force-quit by user (swiped up)?
│  │  └─ System will NOT wake force-quit apps for silent push
│  └─ Suspended or background?
│     └─ Should wake — continue debugging
│
└─ System throttling?
   ├─ Budget: ~2-3 silent pushes per hour
   │  └─ Exceeding? → Reduce frequency, batch updates
   └─ Device in Low Power Mode?
      └─ Further reduces background execution budget
决策树 4:富通知缺少媒体内容
Rich notification not showing image/video?
│
├─ Payload has mutable-content: 1?
│  └─ Missing? → Required for Notification Service Extension to fire
│
├─ Notification Service Extension target exists?
│  ├─ Missing? → File > New > Target > Notification Service Extension
│  └─ Exists → continue
│
├─ Extension bundle ID correct?
│  ├─ Must be: {app-bundle-id}.{extension-name}
│  │  Example: com.myapp.NotificationService
│  └─ Wrong prefix? → Fix bundle ID to match parent app
│
├─ Download completing in time?
│  ├─ Extension has ~30 seconds to modify notification
│  │  └─ Large file? → Use thumbnail URL, not full resolution
│  └─ serviceExtensionTimeWillExpire called?
│     └─ Must deliver bestAttemptContent with fallback text
│
├─ Attachment created correctly?
│  ├─ File written to disk before creating UNNotificationAttachment?
│  │  └─ Must write to tmp directory, then create attachment from file URL
│  └─ File type supported?
│     ├─ Images: JPEG, GIF, PNG (max 10MB)
│     ├─ Audio: AIFF, WAV, MP3, M4A (max 5MB)
│     └─ Video: MPEG, MPEG-2, MP4, AVI (max 50MB)
│
└─ App groups configured?
   └─ Extension and app share data via App Groups?
      └─ Missing? → Add same App Group to both targets
决策树 5:实时活动无法通过推送更新
Live Activity not updating from push?
│
├─ APNs topic correct?
│  ├─ Must be: {bundleID}.push-type.liveactivity
│  │  └─ Using plain bundle ID? → Append .push-type.liveactivity
│  └─ Topic correct → continue
│
├─ Push type header correct?
│  ├─ apns-push-type: liveactivity?
│  │  └─ Using "alert"? → Must be "liveactivity"
│  └─ Correct → continue
│
├─ Content-state matches ActivityAttributes.ContentState?
│  ├─ JSON keys match Swift property names exactly?
│  │  └─ Mismatch? → Decoding fails silently
│  └─ Using custom CodingKeys or JSONEncoder strategies?
│     └─ Custom strategies NOT supported — use default key encoding
│
├─ Push token being sent to server?
│  ├─ Observing pushTokenUpdates on the Activity?
│  │  └─ Missing? → Must iterate Activity.pushTokenUpdates async sequence
│  └─ Token changes when Activity restarts?
│     └─ Must handle token rotation — send updated token to server
│
└─ Rate limiting?
   ├─ Frequent updates: ~10-12 per hour per Activity
   │  └─ Exceeding? → Batch updates, reduce frequency
   └─ Alert updates (sound/vibration): ~3-4 per hour
      └─ Exceeding? → Reserve alerts for critical state changes
树 6:iOS 更新后通知停止
Notifications stopped working after iOS update?
│
├─ Focus mode auto-enabled? (iOS 15+)
│  ├─ Check Settings > Focus
│  │  └─ Focus active? → App may not be in allowed list
│  └─ No Focus active → continue
│
├─ Interruption level filtering?
│  ├─ Default level is .active (may be filtered by Focus)
│  │  └─ Need to break through Focus? → Use .timeSensitive or .critical
│  ├─ .timeSensitive requires capability
│  │  └─ Missing? → Add Time Sensitive Notifications capability
│  └─ .critical requires Apple entitlement
│     └─ Only for health/safety/security apps — apply via Apple Developer
│
├─ Provisional authorization behavior changed?
│  ├─ iOS 15+ provisional notifications appear in Notification Summary
│  │  └─ User may not see them → Request full authorization
│  └─ Was relying on provisional? → Prompt for explicit permission
│
└─ Communication notifications require INSendMessageIntent?
   ├─ iOS 15+ communication notifications need SiriKit intent
   │  └─ Missing? → Donate INSendMessageIntent before showing notification
   └─ Intent donated but still filtered?
      └─ Check that sender is in user's contacts

推送通知控制台工作流

Apple 的推送通知控制台支持无需服务器的测试:

前往控制台
  1. 打开 https://icloud.developer.apple.com/dashboard
  2. 在侧边栏中选择“Push Notifications”
  3. 选择你的应用的 Bundle ID
发送测试通知
  1. 输入设备令牌(来自步骤 2 的诊断)
  2. 选择环境(Sandbox/Production)
  3. 编写负载或使用模板
  4. 发送并查看送达状态
查看送达日志
  1. 从推送请求的响应头中复制 apns-id
  2. 使用推送通知控制台按 apns-id 查询送达状态
  3. 状态显示:已接受、已送达、已丢弃(附带原因)
常见的控制台查询结果
状态含义操作
已送达APNs 已送达设备问题出在设备端(身份验证、专注模式、扩展)
已丢弃:未注册令牌无效重新注册设备
已丢弃:设备令牌不属于此主题Bundle ID 不匹配修正 apns-topic 请求头
已存储设备离线,稍后将送达等待或检查设备连接状态

使用 simctl 测试模拟器

模拟器无法注册远程通知,但你可以测试通知处理:

cat > test-push.apns << 'EOF'
{
  "Simulator Target Bundle": "com.your.bundle.id",
  "aps": {
    "alert": {
      "title": "Test",
      "body": "Hello from simctl"
    },
    "sound": "default",
    "mutable-content": 1
  }
}
EOF

xcrun simctl push booted com.your.bundle.id test-push.apns
模拟器限制
  • ✅ 通知外观和内容
  • ✅ 通知服务扩展处理
  • ✅ 通知内容扩展(自定义 UI)
  • ✅ 操作处理和类别
  • ❌ APNs 令牌注册(始终失败)
  • ❌ 准确测试静默推送唤醒应用
  • ❌ 实时活动推送更新
拖放替代方案

将 .apns 文件直接拖到模拟器窗口即可送达。负载中必须包含 "Simulator Target Bundle" 键。

常见 FCM 诊断

Swizzling 冲突

症状:使用 Firebase 时令牌回调未触发

原因:已禁用方法 Swizzling,但未实现手动转发

诊断:

// Check Info.plist
// FirebaseAppDelegateProxyEnabled = NO means YOU must forward tokens

修复(如果已禁用 Swizzling):

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Messaging.messaging().apnsToken = deviceToken
}
令牌不匹配

症状:服务器有 FCM 令牌,但 APNs 送达失败

原因:FCM 令牌和 APNs 令牌不同。FCM 封装了 APNs 令牌。

诊断:

// FCM token (send this to YOUR server)
Messaging.messaging().token { token, error in
    print("FCM token: \(token ?? "nil")")
}

// APNs token (FCM handles this internally)
// Do NOT send raw APNs token to your server when using FCM
Firebase 控制台中缺少 APNs 密钥

症状:FCM 在 Android 上正常工作,但 iOS 上收不到通知

修复:

  1. Firebase 控制台 → 项目设置 → Cloud Messaging
  2. 上传 APNs 身份验证密钥(.p8)
  3. 输入 Key ID 和 Team ID
  4. 确认 Bundle ID 与应用一致

快速参考表

症状检查项修复方法
完全收不到通知步骤 1:权利配置启用 Push Notification 功能并重新生成描述文件
令牌注册失败步骤 2:回调在 AppDelegate 中实现这两个委托方法
400 BadDeviceToken令牌格式重新注册;检查十六进制编码(无空格、无尖括号)
403 InvalidProviderTokenJWT/证书重新生成 JWT;验证 Key ID、Team ID、Bundle ID
410 Unregistered设备状态应用已卸载或令牌已轮换 — 从服务器移除
开发环境正常,生产环境异常步骤 3:分别使用 curl 测试两者切换 APNs 端点;不同环境的令牌不同
静默推送被忽略负载 + 请求头content-available: 1,push-type: background,priority: 5
缺少富媒体扩展添加 mutable-content: 1,检查扩展 Bundle ID 和超时设置
实时活动信息过时主题格式使用 {bundleID}.push-type.liveactivity 主题
专注模式过滤通知中断级别对重要通知使用 .timeSensitive
FCM 在 iOS 上失败Firebase 控制台上传 .p8 密钥,并填写正确的 Key ID 和 Team ID
未显示操作类别 ID确保负载中的类别标识符与已注册的类别一致

压力场景

场景 1:“服务端团队说问题出在 iOS 端”

背景:推送通知停止工作。后端团队表示他们的负载没有问题,问题肯定出在应用中。

压力:跳过客户端诊断,直接认定服务端没问题。开始重写通知处理代码。

实际情况:55% 的推送失败是权限配置或令牌问题(步骤 1-2),而不是代码错误。服务端可能发送到了错误的环境,或使用了过期令牌。

正确做法:先完成全部 4 个必需的诊断步骤,再修改代码。将 curl 测试(步骤 3)的结果分享给服务端团队——这能客观证明问题出在哪一端。

回应模板:“让我先核实客户端链路——我可以在 5 分钟内分享 curl 测试结果,这样我们双方都能准确知道故障出在哪。”

场景 2:“iOS 更新后通知停止工作,今天就要发布修复”

背景:用户反馈,更新到新的 iOS 版本后,通知停止工作。管理层要求今天就发布热修复。

压力:立即开始调试通知代码。认定是 Apple 弄坏了什么。

实际情况:新的 iOS 版本通常会默认启用专注模式,或更改中断级别过滤设置。15% 的推送失败是由专注模式或中断抑制造成的——你这边不需要修改代码。

正确做法:查看树 6(“iOS 更新后通知停止工作”)。修改代码前,先在测试设备上检查专注模式设置。如果专注模式正在过滤通知,修复方法是在负载中设置正确的 interruption-level,而不是重写通知处理逻辑。

回应模板:“iOS 更新经常会更改专注模式的默认设置。让我先检查中断级别——如果问题出在这里,只需修改一行负载,而不必重写代码。”

场景 3:“静默推送上周还能用,什么都没改”

背景:通过静默推送进行的后台内容同步停止工作了。“我们什么都没改。”

压力:深入检查后台执行代码。认定是回归问题。

实际情况:静默推送有系统强制执行的节流额度(每小时约 2-3 次)。如果使用量增加,或用户强制退出了应用,静默推送就会停止工作,与代码质量无关。此外,配置描述文件可能在重新生成时未包含推送权限。

正确做法:按照树 3(“静默通知未唤醒应用”)进行排查。调试代码前,先检查节流额度、强制退出状态和权限配置。

回应模板:“静默推送有系统节流额度。让我确认我们没有超出额度,并检查测试设备上的应用是否被强制退出——这是两个最常见的原因。”

检查清单

升级处理推送通知问题前:

  • 在 Xcode 中启用 Push Notification 功能(步骤 1)
  • 配置描述文件包含 aps-environment(步骤 1)
  • 令牌注册回调已触发,并收到 64 位十六进制令牌(步骤 2)
  • curl 请求 APNs 返回 HTTP/2 200(步骤 3)
  • 用户已授权通知,状态 = 2(步骤 4)
  • APNs 环境与构建类型匹配(沙盒/生产)
  • 测试设备上的专注模式未过滤通知
  • 已在实体设备上测试(令牌注册不能使用 Simulator)
  • 对于 FCM:已在 Firebase Console 中上传 APNs 身份验证密钥
  • 对于静默推送:已启用后台模式,优先级为 5,且不包含提醒键

资源

WWDC:2021-10091、2023-10025、2023-10185

文档:/usernotifications、/usernotifications/testing-notifications-using-the-push-notification-console

技能:axiom-push-notifications、axiom-push-notifications-ref、axiom-extensions-widgets

评论

支持 Markdown