新增设置页

This commit is contained in:
2026-04-17 14:26:28 +08:00
parent 2cf1fae21d
commit 6a65d956a8
9 changed files with 340 additions and 39 deletions
+16
View File
@@ -283,4 +283,20 @@ export async function getCaregiverActivity(caregiverId: string) {
conversationTurns,
toolCalls,
};
}
export async function getCaregiverSettingsOverview(caregiverId: string) {
const [deviceCount, pushSubscriptionCount] = await prisma.$transaction([
prisma.deviceBinding.count({
where: { caregiverId },
}),
prisma.pushSubscription.count({
where: { caregiverId },
}),
]);
return {
deviceCount,
pushSubscriptionCount,
};
}
+119 -32
View File
@@ -13,6 +13,20 @@ type SerializablePushSubscription = {
};
};
type StoredPushSubscription = {
endpoint: string;
expirationTime: Date | null;
p256dh: string;
auth: string;
};
type PushDeliverySummary = {
targetedCount: number;
sentCount: number;
removedCount: number;
failedCount: number;
};
const APP_BASE_URL = (
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
).replace(/\/+$/, "");
@@ -119,6 +133,70 @@ export async function removePushSubscription(input: {
});
}
async function deliverPushToSubscriptions(input: {
subscriptions: StoredPushSubscription[];
payload: string;
topic: string;
}) {
if (input.subscriptions.length === 0) {
return {
targetedCount: 0,
sentCount: 0,
removedCount: 0,
failedCount: 0,
} satisfies PushDeliverySummary;
}
ensureVapidDetails();
const results = await Promise.all(
input.subscriptions.map(async (subscription) => {
try {
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
expirationTime: subscription.expirationTime?.getTime() || null,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth,
},
},
input.payload,
{
TTL: 60 * 30,
urgency: "high",
topic: input.topic,
},
);
return "sent" as const;
} catch (error) {
const statusCode =
error && typeof error === "object" && "statusCode" in error
? Number((error as { statusCode?: number }).statusCode)
: 0;
if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.deleteMany({
where: { endpoint: subscription.endpoint },
});
return "removed" as const;
}
return "failed" as const;
}
}),
);
return {
targetedCount: input.subscriptions.length,
sentCount: results.filter((result) => result === "sent").length,
removedCount: results.filter((result) => result === "removed").length,
failedCount: results.filter((result) => result === "failed").length,
} satisfies PushDeliverySummary;
}
export async function sendIncomingMessagePush(publicId: number) {
if (!hasPushConfiguration()) {
return;
@@ -172,37 +250,46 @@ export async function sendIncomingMessagePush(publicId: number) {
tag: `message-${message.publicId}`,
});
await Promise.allSettled(
subscriptions.map(async (subscription) => {
try {
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
expirationTime: subscription.expirationTime?.getTime() || null,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth,
},
},
payload,
{
TTL: 60 * 30,
urgency: "high",
topic: `msg-${message.publicId}`,
},
);
} catch (error) {
const statusCode =
error && typeof error === "object" && "statusCode" in error
? Number((error as { statusCode?: number }).statusCode)
: 0;
await deliverPushToSubscriptions({
subscriptions,
payload,
topic: `msg-${message.publicId}`,
});
}
if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.deleteMany({
where: { endpoint: subscription.endpoint },
});
}
}
}),
);
export async function sendCaregiverTestPush(input: {
caregiverId: string;
caregiverLabel: string;
}) {
if (!hasPushConfiguration()) {
throw new Error("当前环境还没有配置推送服务。",
);
}
const subscriptions = await prisma.pushSubscription.findMany({
where: {
caregiverId: input.caregiverId,
},
select: {
endpoint: true,
expirationTime: true,
p256dh: true,
auth: true,
},
});
const payload = JSON.stringify({
title: "安智伴推送测试",
body: `${input.caregiverLabel} 账号下的通知链路工作正常。`,
url: absoluteUrl("/settings"),
icon: absoluteUrl("/pwa/icon-192x192.png"),
badge: absoluteUrl("/pwa/badge-96x96.png"),
tag: `push-test-${input.caregiverId}`,
});
return deliverPushToSubscriptions({
subscriptions,
payload,
topic: `test-${input.caregiverId.slice(0, 20)}`,
});
}