push增加详细消息

This commit is contained in:
feie9456 2026-04-17 14:44:27 +08:00
parent 6a65d956a8
commit 3134aab4dc
2 changed files with 107 additions and 6 deletions

View File

@ -7,6 +7,14 @@ type PushTestPanelProps = {
deviceCount: number; deviceCount: number;
}; };
type PushFailureDetail = {
endpointHost?: string;
statusCode?: number | null;
message?: string;
body?: string | null;
reason?: string | null;
};
export function PushTestPanel({ export function PushTestPanel({
initialSubscriptionCount, initialSubscriptionCount,
deviceCount, deviceCount,
@ -16,10 +24,12 @@ export function PushTestPanel({
initialSubscriptionCount, initialSubscriptionCount,
); );
const [notice, setNotice] = useState<string | null>(null); const [notice, setNotice] = useState<string | null>(null);
const [failureDetails, setFailureDetails] = useState<PushFailureDetail[]>([]);
async function handleTestPush() { async function handleTestPush() {
setSubmitting(true); setSubmitting(true);
setNotice(null); setNotice(null);
setFailureDetails([]);
try { try {
const response = await fetch("/api/push/test", { const response = await fetch("/api/push/test", {
@ -33,6 +43,7 @@ export function PushTestPanel({
sentCount?: number; sentCount?: number;
removedCount?: number; removedCount?: number;
failedCount?: number; failedCount?: number;
failureDetails?: PushFailureDetail[];
} }
| null; | null;
@ -44,8 +55,12 @@ export function PushTestPanel({
const removedCount = payload?.removedCount || 0; const removedCount = payload?.removedCount || 0;
const sentCount = payload?.sentCount || 0; const sentCount = payload?.sentCount || 0;
const failedCount = payload?.failedCount || 0; const failedCount = payload?.failedCount || 0;
const nextFailureDetails = Array.isArray(payload?.failureDetails)
? payload.failureDetails
: [];
setSubscriptionCount(Math.max(0, targetedCount - removedCount)); setSubscriptionCount(Math.max(0, targetedCount - removedCount));
setFailureDetails(nextFailureDetails);
if (targetedCount === 0) { if (targetedCount === 0) {
setNotice("当前账号还没有任何开启消息提醒的设备,先在要接收通知的设备上打开消息提醒。\n"); setNotice("当前账号还没有任何开启消息提醒的设备,先在要接收通知的设备上打开消息提醒。\n");
@ -115,6 +130,28 @@ export function PushTestPanel({
{notice} {notice}
</p> </p>
) : null} ) : null}
{failureDetails.length > 0 ? (
<div className="mt-4 space-y-3">
{failureDetails.map((detail, index) => (
<div
key={`${detail.endpointHost || "unknown"}-${index}`}
className="rounded-[20px] bg-[var(--paper-soft)] px-4 py-4 text-sm leading-7 text-[var(--muted)]"
>
<p className="font-semibold text-[var(--ink)]">
{detail.endpointHost || "unknown"}
{typeof detail.statusCode === "number"
? ` · HTTP ${detail.statusCode}`
: ""}
</p>
<p className="mt-1">
{detail.reason || detail.message || "推送发送失败"}
</p>
{detail.body ? <p className="mt-1 break-all">{detail.body}</p> : null}
</div>
))}
</div>
) : null}
</section> </section>
); );
} }

View File

@ -27,6 +27,18 @@ type PushDeliverySummary = {
failedCount: number; 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 = ( const APP_BASE_URL = (
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net" process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
).replace(/\/+$/, ""); ).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: { async function deliverPushToSubscriptions(input: {
subscriptions: StoredPushSubscription[]; subscriptions: StoredPushSubscription[];
payload: string; payload: string;
topic: string; topic: string;
collectFailureDetails?: boolean;
}) { }) {
if (input.subscriptions.length === 0) { if (input.subscriptions.length === 0) {
return { return {
@ -144,11 +199,14 @@ async function deliverPushToSubscriptions(input: {
sentCount: 0, sentCount: 0,
removedCount: 0, removedCount: 0,
failedCount: 0, failedCount: 0,
} satisfies PushDeliverySummary; failureDetails: [],
} satisfies PushDeliveryResult;
} }
ensureVapidDetails(); ensureVapidDetails();
const failureDetails: PushFailureDetail[] = [];
const results = await Promise.all( const results = await Promise.all(
input.subscriptions.map(async (subscription) => { input.subscriptions.map(async (subscription) => {
try { try {
@ -171,10 +229,14 @@ async function deliverPushToSubscriptions(input: {
return "sent" as const; return "sent" as const;
} catch (error) { } catch (error) {
const statusCode = const detail = getPushFailureDetail(subscription, error);
error && typeof error === "object" && "statusCode" in error const statusCode = detail.statusCode || 0;
? Number((error as { statusCode?: number }).statusCode)
: 0; console.error("Push notification delivery failed", detail);
if (input.collectFailureDetails) {
failureDetails.push(detail);
}
if (statusCode === 404 || statusCode === 410) { if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.deleteMany({ await prisma.pushSubscription.deleteMany({
@ -194,7 +256,8 @@ async function deliverPushToSubscriptions(input: {
sentCount: results.filter((result) => result === "sent").length, sentCount: results.filter((result) => result === "sent").length,
removedCount: results.filter((result) => result === "removed").length, removedCount: results.filter((result) => result === "removed").length,
failedCount: results.filter((result) => result === "failed").length, failedCount: results.filter((result) => result === "failed").length,
} satisfies PushDeliverySummary; failureDetails,
} satisfies PushDeliveryResult;
} }
export async function sendIncomingMessagePush(publicId: number) { export async function sendIncomingMessagePush(publicId: number) {
@ -291,5 +354,6 @@ export async function sendCaregiverTestPush(input: {
subscriptions, subscriptions,
payload, payload,
topic: `test-${input.caregiverId.slice(0, 20)}`, topic: `test-${input.caregiverId.slice(0, 20)}`,
collectFailureDetails: true,
}); });
} }