From 3134aab4dcbcb4310eb6a94ed49ff24b8b237b25 Mon Sep 17 00:00:00 2001
From: feie9456
Date: Fri, 17 Apr 2026 14:44:27 +0800
Subject: [PATCH] =?UTF-8?q?push=E5=A2=9E=E5=8A=A0=E8=AF=A6=E7=BB=86?=
=?UTF-8?q?=E6=B6=88=E6=81=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components/push-test-panel.tsx | 37 +++++++++++++++++
lib/push.ts | 76 +++++++++++++++++++++++++++++++---
2 files changed, 107 insertions(+), 6 deletions(-)
diff --git a/components/push-test-panel.tsx b/components/push-test-panel.tsx
index fa6849a..335df0c 100644
--- a/components/push-test-panel.tsx
+++ b/components/push-test-panel.tsx
@@ -7,6 +7,14 @@ type PushTestPanelProps = {
deviceCount: number;
};
+type PushFailureDetail = {
+ endpointHost?: string;
+ statusCode?: number | null;
+ message?: string;
+ body?: string | null;
+ reason?: string | null;
+};
+
export function PushTestPanel({
initialSubscriptionCount,
deviceCount,
@@ -16,10 +24,12 @@ export function PushTestPanel({
initialSubscriptionCount,
);
const [notice, setNotice] = useState(null);
+ const [failureDetails, setFailureDetails] = useState([]);
async function handleTestPush() {
setSubmitting(true);
setNotice(null);
+ setFailureDetails([]);
try {
const response = await fetch("/api/push/test", {
@@ -33,6 +43,7 @@ export function PushTestPanel({
sentCount?: number;
removedCount?: number;
failedCount?: number;
+ failureDetails?: PushFailureDetail[];
}
| null;
@@ -44,8 +55,12 @@ export function PushTestPanel({
const removedCount = payload?.removedCount || 0;
const sentCount = payload?.sentCount || 0;
const failedCount = payload?.failedCount || 0;
+ const nextFailureDetails = Array.isArray(payload?.failureDetails)
+ ? payload.failureDetails
+ : [];
setSubscriptionCount(Math.max(0, targetedCount - removedCount));
+ setFailureDetails(nextFailureDetails);
if (targetedCount === 0) {
setNotice("当前账号还没有任何开启消息提醒的设备,先在要接收通知的设备上打开消息提醒。\n");
@@ -115,6 +130,28 @@ export function PushTestPanel({
{notice}
) : null}
+
+ {failureDetails.length > 0 ? (
+
+ {failureDetails.map((detail, index) => (
+
+
+ {detail.endpointHost || "unknown"}
+ {typeof detail.statusCode === "number"
+ ? ` · HTTP ${detail.statusCode}`
+ : ""}
+
+
+ {detail.reason || detail.message || "推送发送失败"}
+
+ {detail.body ?
{detail.body}
: null}
+
+ ))}
+
+ ) : null}
);
}
\ No newline at end of file
diff --git a/lib/push.ts b/lib/push.ts
index c34bfd9..e92331f 100644
--- a/lib/push.ts
+++ b/lib/push.ts
@@ -27,6 +27,18 @@ type PushDeliverySummary = {
failedCount: number;
};
+type PushFailureDetail = {
+ endpointHost: string;
+ statusCode: number | null;
+ message: string;
+ body: string | null;
+ reason: string | null;
+};
+
+type PushDeliveryResult = PushDeliverySummary & {
+ failureDetails?: PushFailureDetail[];
+};
+
const APP_BASE_URL = (
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
).replace(/\/+$/, "");
@@ -133,10 +145,53 @@ export async function removePushSubscription(input: {
});
}
+function getEndpointHost(endpoint: string) {
+ try {
+ return new URL(endpoint).host;
+ } catch {
+ return "unknown";
+ }
+}
+
+function getPushFailureDetail(subscription: StoredPushSubscription, error: unknown) {
+ const statusCode =
+ error && typeof error === "object" && "statusCode" in error
+ ? Number((error as { statusCode?: number }).statusCode)
+ : null;
+ const message =
+ error && typeof error === "object" && "message" in error
+ ? String((error as { message?: string }).message || "推送发送失败")
+ : "推送发送失败";
+ const body =
+ error && typeof error === "object" && "body" in error
+ ? String((error as { body?: string }).body || "") || null
+ : null;
+
+ let reason: string | null = null;
+
+ if (body) {
+ try {
+ const parsedBody = JSON.parse(body) as { reason?: unknown };
+ reason = typeof parsedBody.reason === "string" ? parsedBody.reason : null;
+ } catch {
+ reason = null;
+ }
+ }
+
+ return {
+ endpointHost: getEndpointHost(subscription.endpoint),
+ statusCode,
+ message,
+ body,
+ reason,
+ } satisfies PushFailureDetail;
+}
+
async function deliverPushToSubscriptions(input: {
subscriptions: StoredPushSubscription[];
payload: string;
topic: string;
+ collectFailureDetails?: boolean;
}) {
if (input.subscriptions.length === 0) {
return {
@@ -144,11 +199,14 @@ async function deliverPushToSubscriptions(input: {
sentCount: 0,
removedCount: 0,
failedCount: 0,
- } satisfies PushDeliverySummary;
+ failureDetails: [],
+ } satisfies PushDeliveryResult;
}
ensureVapidDetails();
+ const failureDetails: PushFailureDetail[] = [];
+
const results = await Promise.all(
input.subscriptions.map(async (subscription) => {
try {
@@ -171,10 +229,14 @@ async function deliverPushToSubscriptions(input: {
return "sent" as const;
} catch (error) {
- const statusCode =
- error && typeof error === "object" && "statusCode" in error
- ? Number((error as { statusCode?: number }).statusCode)
- : 0;
+ const detail = getPushFailureDetail(subscription, error);
+ const statusCode = detail.statusCode || 0;
+
+ console.error("Push notification delivery failed", detail);
+
+ if (input.collectFailureDetails) {
+ failureDetails.push(detail);
+ }
if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.deleteMany({
@@ -194,7 +256,8 @@ async function deliverPushToSubscriptions(input: {
sentCount: results.filter((result) => result === "sent").length,
removedCount: results.filter((result) => result === "removed").length,
failedCount: results.filter((result) => result === "failed").length,
- } satisfies PushDeliverySummary;
+ failureDetails,
+ } satisfies PushDeliveryResult;
}
export async function sendIncomingMessagePush(publicId: number) {
@@ -291,5 +354,6 @@ export async function sendCaregiverTestPush(input: {
subscriptions,
payload,
topic: `test-${input.caregiverId.slice(0, 20)}`,
+ collectFailureDetails: true,
});
}
\ No newline at end of file