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