push-notifications by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill push-notifications使用 UserNotifications 和 APNs 在 iOS/macOS 上实现、审查和调试本地及远程通知。涵盖权限流程、令牌注册、负载结构、前台处理、通知操作、分组和富通知。目标为 iOS 26+ 和 Swift 6.2,除非特别说明,向后兼容至 iOS 16。
在执行任何其他操作之前,请求通知授权。系统提示仅出现一次;后续调用将返回存储的决定。
import UserNotifications
@MainActor
func requestNotificationPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
do {
let granted = try await center.requestAuthorization(
options: [.alert, .sound, .badge]
)
return granted
} catch {
print("Authorization request failed: \(error)")
return false
}
}
在假设拥有权限之前,务必检查状态。用户可以随时更改设置。
@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
let settings = await UNUserNotificationCenter.current().notificationSettings()
return settings.authorizationStatus
// .notDetermined, .denied, .authorized, .provisional, .ephemeral
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
临时通知会静默发送到通知中心,不会打扰用户。然后用户可以选择保留或关闭它们。用于引导流程,希望在请求完全权限之前展示其价值。
// 静默发送 -- 不会向用户显示权限提示
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])
关键警报会绕过“勿扰模式”和静音开关。需要 Apple 的特殊授权(通过开发者门户请求)。仅用于健康、安全或安全场景。
// 需要 com.apple.developer.usernotifications.critical-alerts 授权
try await center.requestAuthorization(
options: [.alert, .sound, .badge, .criticalAlert]
)
当用户拒绝了通知权限时,引导他们前往“设置”。不要重复提示或纠缠。
struct NotificationSettingsButton: View {
@Environment(\.openURL) private var openURL
var body: some View {
Button("打开设置") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
在 SwiftUI 应用中使用 UIApplicationDelegateAdaptor 来接收设备令牌。AppDelegate 回调是接收 APNs 令牌的唯一方式。
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs token: \(token)")
// 将令牌发送到你的服务器
Task { await TokenService.shared.upload(token: token) }
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error.localizedDescription)")
// 模拟器总是失败 -- 这在开发过程中是预期情况
}
}
首先请求授权,然后注册远程通知。注册会触发系统联系 APNs 并返回设备令牌。
@MainActor
func registerForPush() async {
let granted = await requestNotificationPermission()
guard granted else { return }
UIApplication.shared.registerForRemoteNotifications()
}
设备令牌会改变。每次触发 didRegisterForRemoteNotificationsWithDeviceToken 时,都要将令牌重新发送到你的服务器,而不仅仅是第一次。系统在每次调用 registerForRemoteNotifications() 的应用启动时都会调用此方法。
无需服务器,直接从设备安排通知。适用于提醒、计时器和基于位置的警报。
let content = UNMutableNotificationContent()
content.title = "锻炼提醒"
content.subtitle = "该活动了"
content.body = "您有一个安排在 15 分钟后的锻炼。"
content.sound = .default
content.badge = 1
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts" // 在通知中心分组
// 在一段时间间隔后触发(重复触发的最小间隔为 60 秒)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
// 在特定日期/时间触发
var dateComponents = DateComponents()
dateComponents.hour = 8
dateComponents.minute = 30
let calendarTrigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents, repeats: true // 每天上午 8:30
)
// 当进入地理区域时触发
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
radius: 100,
identifier: "gym"
)
region.notifyOnEntry = true
region.notifyOnExit = false
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
// 至少需要 "使用时" 位置权限
let request = UNNotificationRequest(
identifier: "workout-reminder-abc123",
content: content,
trigger: timeTrigger
)
let center = UNUserNotificationCenter.current()
try await center.add(request)
// 移除特定的待处理通知
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])
// 移除所有待处理通知
center.removeAllPendingNotificationRequests()
// 从通知中心移除已送达的通知
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
center.removeAllDeliveredNotifications()
// 列出所有待处理的请求
let pending = await center.pendingNotificationRequests()
{
"aps": {
"alert": {
"title": "新消息",
"subtitle": "来自 Alice",
"body": "嘿,你中午有空吗?"
},
"badge": 3,
"sound": "default",
"thread-id": "chat-alice",
"category": "MESSAGE_CATEGORY"
},
"messageId": "msg-789",
"senderId": "user-alice"
}
设置 content-available: 1,不包含 alert、sound 或 badge。系统会在后台唤醒应用。需要 "Background Modes > Remote notifications" 能力。
{
"aps": {
"content-available": 1
},
"updateType": "new-data"
}
在 AppDelegate 中处理:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
guard let updateType = userInfo["updateType"] as? String else {
return .noData
}
do {
try await DataSyncService.shared.sync(trigger: updateType)
return .newData
} catch {
return .failed
}
}
设置 mutable-content: 1 以允许通知服务扩展在显示前修改内容。用于下载图像、解密内容或添加附件。
{
"aps": {
"alert": { "title": "照片", "body": "Alice 发送了一张照片" },
"mutable-content": 1
},
"imageUrl": "https://example.com/photo.jpg"
}
使用本地化键,以便通知以用户的语言显示:
{
"aps": {
"alert": {
"title-loc-key": "NEW_MESSAGE_TITLE",
"loc-key": "NEW_MESSAGE_BODY",
"loc-args": ["Alice"]
}
}
}
实现委托以控制前台显示并处理用户点击。尽早设置委托 —— 在 application(_:didFinishLaunchingWithOptions:) 或 App.init 中。
@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
static let shared = NotificationDelegate()
// 当应用处于前台时通知到达时调用
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
// 返回要显示的呈现元素
// 如果没有此方法,前台通知将被静默抑制
return [.banner, .sound, .badge]
}
// 当用户点击通知时调用
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
let userInfo = response.notification.request.content.userInfo
let actionIdentifier = response.actionIdentifier
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// 用户点击了通知主体
await handleNotificationTap(userInfo: userInfo)
case UNNotificationDismissActionIdentifier:
// 用户关闭了通知
break
default:
// 点击了自定义操作按钮
await handleCustomAction(actionIdentifier, userInfo: userInfo)
}
}
}
使用共享的 @Observable 路由器将通知点击路由到正确的屏幕。委托写入一个待处理的目标;SwiftUI 视图观察并消费它。
@Observable @MainActor
final class DeepLinkRouter {
var pendingDestination: AppDestination?
}
// 在 NotificationDelegate 中:
func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
guard let id = userInfo["messageId"] as? String else { return }
DeepLinkRouter.shared.pendingDestination = .chat(id: id)
}
// 在 SwiftUI 中 -- 观察并消费:
.onChange(of: router.pendingDestination) { _, destination in
if let destination {
path.append(destination)
router.pendingDestination = nil
}
}
有关包含标签页切换的完整深度链接处理程序,请参阅 references/notification-patterns.md。
定义作为按钮出现在通知上的交互式操作。在启动时注册类别。
func registerNotificationCategories() {
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "回复",
options: [],
textInputButtonTitle: "发送",
textInputPlaceholder: "输入回复..."
)
let likeAction = UNNotificationAction(
identifier: "LIKE_ACTION",
title: "点赞",
options: []
)
let deleteAction = UNNotificationAction(
identifier: "DELETE_ACTION",
title: "删除",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction, likeAction, deleteAction],
intentIdentifiers: [],
options: [.customDismissAction] // 关闭时也会触发 didReceive
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
switch identifier {
case "REPLY_ACTION":
// 对于文本输入操作,response 是 UNTextInputNotificationResponse
break
case "LIKE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.likeMessage(id: messageId)
case "DELETE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.deleteMessage(id: messageId)
default:
break
}
}
操作选项:
.authenticationRequired -- 设备必须解锁才能执行操作.destructive -- 以红色显示;用于删除/移除操作.foreground -- 点击时将应用启动到前台使用 threadIdentifier(或 APNs 负载中的 thread-id)对相关通知进行分组。每个唯一的线程在通知中心中成为一个单独的组。
content.threadIdentifier = "chat-alice" // 来自 Alice 的所有消息分组在一起
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3 // "来自 Alice 的 3 条更多通知"
在类别中自定义摘要格式字符串:
let category = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction],
intentIdentifiers: [],
categorySummaryFormat: "%u 条来自 %@ 的更多消息",
options: []
)
不要: 在请求授权之前注册远程通知。要: 先调用 requestAuthorization,然后调用 registerForRemoteNotifications()。
不要: 使用 String(data: deviceToken, encoding: .utf8) 转换设备令牌。要: 使用十六进制:deviceToken.map { String(format: "%02x", $0) }.joined()。
不要: 假设通知总是能到达。APNs 是尽力而为的。要: 设计能够优雅降级的功能;使用后台刷新作为后备方案。
不要: 将敏感数据直接放在通知负载中。要: 使用 mutable-content: 1 配合通知服务扩展。
不要: 忘记前台处理。没有 willPresent,通知会被静默抑制。要: 实现 willPresent 并返回 .banner, .sound, .badge。
不要: 设置委托太晚,或者在没有 AppDelegate 适配器的情况下从 SwiftUI 视图注册。要: 在 App.init 中设置委托;使用 UIApplicationDelegateAdaptor 处理 APNs。
不要: 只发送一次设备令牌 —— 令牌会改变。每次回调时都要重新发送。
String(data:encoding:))UNUserNotificationCenterDelegate 已在 App.init 或 application(_:didFinishLaunching:) 中设置willPresent)和点击(didReceive)处理已实现UIApplicationDelegateAdaptor 处理 APNsreferences/notification-patterns.md — AppDelegate 设置、APNs 回调、深度链接路由器、静默推送、调试references/rich-notifications.md — 服务扩展、内容扩展、附件、通信通知每周安装数
398
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex394
kimi-cli391
amp391
cline391
github-copilot391
opencode391
Implement, review, and debug local and remote notifications on iOS/macOS using UserNotifications and APNs. Covers permission flow, token registration, payload structure, foreground handling, notification actions, grouping, and rich notifications. Targets iOS 26+ with Swift 6.2, backward-compatible to iOS 16 unless noted.
Request notification authorization before doing anything else. The system prompt appears only once; subsequent calls return the stored decision.
import UserNotifications
@MainActor
func requestNotificationPermission() async -> Bool {
let center = UNUserNotificationCenter.current()
do {
let granted = try await center.requestAuthorization(
options: [.alert, .sound, .badge]
)
return granted
} catch {
print("Authorization request failed: \(error)")
return false
}
}
Always check status before assuming permissions. The user can change settings at any time.
@MainActor
func checkNotificationStatus() async -> UNAuthorizationStatus {
let settings = await UNUserNotificationCenter.current().notificationSettings()
return settings.authorizationStatus
// .notDetermined, .denied, .authorized, .provisional, .ephemeral
}
Provisional notifications deliver quietly to the notification center without interrupting the user. The user can then choose to keep or turn them off. Use for onboarding flows where you want to demonstrate value before asking for full permission.
// Delivers silently -- no permission prompt shown to the user
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])
Critical alerts bypass Do Not Disturb and the mute switch. Requires a special entitlement from Apple (request via developer portal). Use only for health, safety, or security scenarios.
// Requires com.apple.developer.usernotifications.critical-alerts entitlement
try await center.requestAuthorization(
options: [.alert, .sound, .badge, .criticalAlert]
)
When the user has denied notifications, guide them to Settings. Do not repeatedly prompt or nag.
struct NotificationSettingsButton: View {
@Environment(\.openURL) private var openURL
var body: some View {
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
Use UIApplicationDelegateAdaptor to receive the device token in a SwiftUI app. The AppDelegate callbacks are the only way to receive APNs tokens.
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("APNs token: \(token)")
// Send token to your server
Task { await TokenService.shared.upload(token: token) }
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
print("APNs registration failed: \(error.localizedDescription)")
// Simulator always fails -- this is expected during development
}
}
Request authorization first, then register for remote notifications. Registration triggers the system to contact APNs and return a device token.
@MainActor
func registerForPush() async {
let granted = await requestNotificationPermission()
guard granted else { return }
UIApplication.shared.registerForRemoteNotifications()
}
Device tokens change. Re-send the token to your server every time didRegisterForRemoteNotificationsWithDeviceToken fires, not just the first time. The system calls this method on every app launch that calls registerForRemoteNotifications().
Schedule notifications directly from the device without a server. Useful for reminders, timers, and location-based alerts.
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.subtitle = "Time to move"
content.body = "You have a scheduled workout in 15 minutes."
content.sound = .default
content.badge = 1
content.userInfo = ["workoutId": "abc123"]
content.threadIdentifier = "workouts" // groups in notification center
// Fire after a time interval (minimum 60 seconds for repeating)
let timeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 300, repeats: false)
// Fire at a specific date/time
var dateComponents = DateComponents()
dateComponents.hour = 8
dateComponents.minute = 30
let calendarTrigger = UNCalendarNotificationTrigger(
dateMatching: dateComponents, repeats: true // daily at 8:30 AM
)
// Fire when entering a geographic region
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01),
radius: 100,
identifier: "gym"
)
region.notifyOnEntry = true
region.notifyOnExit = false
let locationTrigger = UNLocationNotificationTrigger(region: region, repeats: false)
// Requires "When In Use" location permission at minimum
let request = UNNotificationRequest(
identifier: "workout-reminder-abc123",
content: content,
trigger: timeTrigger
)
let center = UNUserNotificationCenter.current()
try await center.add(request)
// Remove specific pending notifications
center.removePendingNotificationRequests(withIdentifiers: ["workout-reminder-abc123"])
// Remove all pending
center.removeAllPendingNotificationRequests()
// Remove delivered notifications from notification center
center.removeDeliveredNotifications(withIdentifiers: ["workout-reminder-abc123"])
center.removeAllDeliveredNotifications()
// List all pending requests
let pending = await center.pendingNotificationRequests()
{
"aps": {
"alert": {
"title": "New Message",
"subtitle": "From Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default",
"thread-id": "chat-alice",
"category": "MESSAGE_CATEGORY"
},
"messageId": "msg-789",
"senderId": "user-alice"
}
Set content-available: 1 with no alert, sound, or badge. The system wakes the app in the background. Requires the "Background Modes > Remote notifications" capability.
{
"aps": {
"content-available": 1
},
"updateType": "new-data"
}
Handle in AppDelegate:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
guard let updateType = userInfo["updateType"] as? String else {
return .noData
}
do {
try await DataSyncService.shared.sync(trigger: updateType)
return .newData
} catch {
return .failed
}
}
Set mutable-content: 1 to allow a Notification Service Extension to modify content before display. Use for downloading images, decrypting content, or adding attachments.
{
"aps": {
"alert": { "title": "Photo", "body": "Alice sent a photo" },
"mutable-content": 1
},
"imageUrl": "https://example.com/photo.jpg"
}
Use localization keys so the notification displays in the user's language:
{
"aps": {
"alert": {
"title-loc-key": "NEW_MESSAGE_TITLE",
"loc-key": "NEW_MESSAGE_BODY",
"loc-args": ["Alice"]
}
}
}
Implement the delegate to control foreground display and handle user taps. Set the delegate as early as possible -- in application(_:didFinishLaunchingWithOptions:) or App.init.
@MainActor
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
static let shared = NotificationDelegate()
// Called when notification arrives while app is in FOREGROUND
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
// Return which presentation elements to show
// Without this, foreground notifications are silently suppressed
return [.banner, .sound, .badge]
}
// Called when user TAPS the notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
let userInfo = response.notification.request.content.userInfo
let actionIdentifier = response.actionIdentifier
switch actionIdentifier {
case UNNotificationDefaultActionIdentifier:
// User tapped the notification body
await handleNotificationTap(userInfo: userInfo)
case UNNotificationDismissActionIdentifier:
// User dismissed the notification
break
default:
// Custom action button tapped
await handleCustomAction(actionIdentifier, userInfo: userInfo)
}
}
}
Route notification taps to the correct screen using a shared @Observable router. The delegate writes a pending destination; the SwiftUI view observes and consumes it.
@Observable @MainActor
final class DeepLinkRouter {
var pendingDestination: AppDestination?
}
// In NotificationDelegate:
func handleNotificationTap(userInfo: [AnyHashable: Any]) async {
guard let id = userInfo["messageId"] as? String else { return }
DeepLinkRouter.shared.pendingDestination = .chat(id: id)
}
// In SwiftUI -- observe and consume:
.onChange(of: router.pendingDestination) { _, destination in
if let destination {
path.append(destination)
router.pendingDestination = nil
}
}
See references/notification-patterns.md for the full deep-linking handler with tab switching.
Define interactive actions that appear as buttons on the notification. Register categories at launch.
func registerNotificationCategories() {
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a reply..."
)
let likeAction = UNNotificationAction(
identifier: "LIKE_ACTION",
title: "Like",
options: []
)
let deleteAction = UNNotificationAction(
identifier: "DELETE_ACTION",
title: "Delete",
options: [.destructive, .authenticationRequired]
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction, likeAction, deleteAction],
intentIdentifiers: [],
options: [.customDismissAction] // fires didReceive on dismiss too
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
func handleCustomAction(_ identifier: String, userInfo: [AnyHashable: Any]) async {
switch identifier {
case "REPLY_ACTION":
// response is UNTextInputNotificationResponse for text input actions
break
case "LIKE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.likeMessage(id: messageId)
case "DELETE_ACTION":
guard let messageId = userInfo["messageId"] as? String else { return }
await MessageService.shared.deleteMessage(id: messageId)
default:
break
}
}
Action options:
.authenticationRequired -- device must be unlocked to perform the action.destructive -- displayed in red; use for delete/remove actions.foreground -- launches the app to the foreground when tappedGroup related notifications with threadIdentifier (or thread-id in the APNs payload). Each unique thread becomes a separate group in Notification Center.
content.threadIdentifier = "chat-alice" // all messages from Alice group together
content.summaryArgument = "Alice"
content.summaryArgumentCount = 3 // "3 more notifications from Alice"
Customize the summary format string in the category:
let category = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [replyAction],
intentIdentifiers: [],
categorySummaryFormat: "%u more messages from %@",
options: []
)
DON'T: Register for remote notifications before requesting authorization. DO: Call requestAuthorization first, then registerForRemoteNotifications().
DON'T: Convert device token with String(data: deviceToken, encoding: .utf8). DO: Use hex: deviceToken.map { String(format: "%02x", $0) }.joined().
DON'T: Assume notifications always arrive. APNs is best-effort. DO: Design features that degrade gracefully; use background refresh as fallback.
DON'T: Put sensitive data directly in the notification payload. DO: Use mutable-content: 1 with a Notification Service Extension.
DON'T: Forget foreground handling. Without willPresent, notifications are silently suppressed. DO: Implement willPresent and return .banner, .sound, .badge.
DON'T: Set delegate too late or register from SwiftUI views without AppDelegate adaptor. DO: Set delegate in App.init; use UIApplicationDelegateAdaptor for APNs.
DON'T: Send device token only once — tokens change. Re-send on every callback.
String(data:encoding:))UNUserNotificationCenterDelegate set in App.init or application(_:didFinishLaunching:)willPresent) and tap (didReceive) handling implementedUIApplicationDelegateAdaptor for APNsreferences/notification-patterns.md — AppDelegate setup, APNs callbacks, deep-link router, silent push, debuggingreferences/rich-notifications.md — Service Extension, Content Extension, attachments, communication notificationsWeekly Installs
398
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex394
kimi-cli391
amp391
cline391
github-copilot391
opencode391
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装