吸收数字员工系统桌面通知

This commit is contained in:
baiyanyun
2026-06-11 20:53:46 +08:00
parent 8278fdc81c
commit 245a210b25
14 changed files with 370 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ import {
patchNotificationPreferences,
pullNotificationUpdates,
replyToNotification,
showDesktopNotification,
type NotificationPreferences,
type UserNotification,
} from '@/lib/api';
@@ -47,10 +48,13 @@ const NOTIFICATION_KIND_OPTIONS: Array<{ id: UserNotification['kind']; label: st
const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences = {
enabled: true,
desktop_enabled: true,
muted_kinds: [],
muted_levels: [],
updated_at: null,
};
const DESKTOP_NOTIFICATION_DEDUPE_KEY = 'qimingclaw:digital-desktop-notified:v1';
const DESKTOP_NOTIFICATION_LEVELS = new Set<UserNotification['level']>(['action', 'warn', 'error']);
function isDigitalTab(value: string | null): value is DigitalTab {
return Boolean(value && TABS.some(tab => tab.id === value));
@@ -122,6 +126,51 @@ function notificationTime(value: string): string {
return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(date);
}
function readDesktopNotificationDedupe(): Set<string> {
try {
const raw = window.localStorage.getItem(DESKTOP_NOTIFICATION_DEDUPE_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return new Set(Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : []);
} catch {
return new Set();
}
}
function writeDesktopNotificationDedupe(ids: Set<string>): void {
try {
window.localStorage.setItem(DESKTOP_NOTIFICATION_DEDUPE_KEY, JSON.stringify(Array.from(ids).slice(-300)));
} catch {
// localStorage may be unavailable in restricted webviews; desktop notification is best effort.
}
}
function notificationMatchesDesktopPreferences(notification: UserNotification, preferences: NotificationPreferences): boolean {
if (!preferences.enabled || !preferences.desktop_enabled) return false;
if (notification.status !== 'unread') return false;
if (!DESKTOP_NOTIFICATION_LEVELS.has(notification.level)) return false;
if (preferences.muted_levels.includes(notification.level)) return false;
return !preferences.muted_kinds.includes(notification.kind);
}
async function showUnreadDesktopNotifications(notifications: UserNotification[], preferences: NotificationPreferences): Promise<void> {
const notified = readDesktopNotificationDedupe();
const candidates = notifications.filter((notification) => (
notificationMatchesDesktopPreferences(notification, preferences)
&& !notified.has(notification.notification_id)
));
if (candidates.length === 0) return;
candidates.forEach((notification) => notified.add(notification.notification_id));
writeDesktopNotificationDedupe(notified);
await Promise.all(candidates.slice(0, 3).map((notification) => showDesktopNotification({
notification_id: notification.notification_id,
title: notification.title,
body: notification.body,
level: notification.level,
kind: notification.kind,
status: notification.status,
}).catch(() => null)));
}
export default function DigitalEmployeeShell() {
const location = useLocation();
const navigate = useNavigate();
@@ -138,15 +187,15 @@ export default function DigitalEmployeeShell() {
const refreshNotifications = useCallback(async () => {
setNotificationsLoading(true);
try {
const [items, count] = await Promise.all([
const [items, count, preferences] = await Promise.all([
getNotifications('all'),
getUnreadNotificationCount(),
getNotificationPreferences().catch(() => DEFAULT_NOTIFICATION_PREFERENCES),
]);
setNotifications(items);
setUnreadCount(count);
void getNotificationPreferences()
.then(setNotificationPreferences)
.catch(() => setNotificationPreferences(DEFAULT_NOTIFICATION_PREFERENCES));
setNotificationPreferences(preferences);
void showUnreadDesktopNotifications(items, preferences);
} catch {
setNotifications([]);
setUnreadCount(0);
@@ -306,6 +355,14 @@ export default function DigitalEmployeeShell() {
/>
<span></span>
</label>
<label className="de-notification-switch">
<input
type="checkbox"
checked={notificationPreferences.desktop_enabled}
onChange={(event) => { void updateNotificationPreferences({ desktop_enabled: event.target.checked }); }}
/>
<span></span>
</label>
<div className="de-notification-preference-group">
<span></span>
<div className="de-notification-segment-row">