吸收数字员工通知订阅策略

This commit is contained in:
baiyanyun
2026-06-10 15:56:34 +08:00
parent 13e48df63d
commit 5b2f14123e
14 changed files with 597 additions and 195 deletions

View File

@@ -12,6 +12,7 @@ import type {
Session,
ChannelDetail,
BrowserSession,
NotificationPreferences,
UserNotification,
DebugStoreResponse,
DebugPlan,
@@ -542,7 +543,7 @@ export function getEventLog(params: {
// Notifications
// ---------------------------------------------------------------------------
export { type UserNotification };
export { type NotificationPreferences, type UserNotification };
export function getNotifications(
status: string = 'unread',
@@ -558,6 +559,19 @@ export function getUnreadNotificationCount(): Promise<number> {
);
}
export function getNotificationPreferences(): Promise<NotificationPreferences> {
return apiFetch<NotificationPreferences>('/api/notifications/preferences');
}
export function patchNotificationPreferences(
patch: Partial<NotificationPreferences>,
): Promise<NotificationPreferences> {
return apiFetch<NotificationPreferences>('/api/notifications/preferences', {
method: 'PATCH',
body: JSON.stringify(patch),
});
}
export function markNotificationRead(id: string): Promise<boolean> {
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
method: 'POST',

View File

@@ -31,6 +31,7 @@ import type {
IngressRouteRequest,
IngressRouteResponse,
MemoryEntry,
NotificationPreferences,
PlanDispatchResponse,
PlanStepSummary,
RouteDecisionRecord,
@@ -85,6 +86,7 @@ interface AdapterState {
reportGeneratedAtByDate: Record<string, string>;
decisionResultsByDate: Record<string, Record<string, string>>;
notificationStateById?: Record<string, NotificationState>;
notificationPreferences?: NotificationPreferences;
consoleRole?: string;
rolePreferences?: Record<string, unknown>;
profile: {
@@ -103,6 +105,18 @@ interface NotificationState {
replied_at?: string | null;
}
const NOTIFICATION_LEVELS: UserNotification['level'][] = ['info', 'warn', 'error', 'action'];
const NOTIFICATION_KINDS: UserNotification['kind'][] = [
'user_message',
'need_input',
'need_approval',
'task_progress',
'task_failed',
'task_finished',
'confirmation_pending',
'confirmation_resolved',
];
interface QimingclawServiceStatus {
running?: boolean;
error?: string;
@@ -836,6 +850,15 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'GET' && pathname === '/api/notifications') {
return buildNotificationsResponse(url.searchParams.get('status'), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/notifications/preferences') {
return notificationPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T;
}
if (method === 'PATCH' && pathname === '/api/notifications/preferences') {
return await handleNotificationPreferencesPatch(
parseJsonBody<Partial<NotificationPreferences>>(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'GET' && pathname === '/api/notifications/unread-count') {
const snapshot = await readQimingclawSnapshot();
return { count: buildNotifications(snapshot, notificationAdapterState(snapshot)).filter((notification) => notification.status === 'unread').length } as T;
@@ -922,6 +945,7 @@ function defaultState(): AdapterState {
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
notificationStateById: {},
notificationPreferences: defaultNotificationPreferences(),
consoleRole: 'sales',
rolePreferences: {},
profile: {
@@ -936,12 +960,20 @@ function defaultState(): AdapterState {
function readState(): AdapterState {
try {
const raw = window.localStorage.getItem(STATE_KEY);
return raw ? { ...defaultState(), ...JSON.parse(raw) as Partial<AdapterState> } : defaultState();
return normalizeAdapterState(raw ? JSON.parse(raw) as Partial<AdapterState> : {});
} catch {
return defaultState();
}
}
function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
return {
...defaultState(),
...value,
notificationPreferences: notificationPreferences(value),
};
}
function writeState(state: AdapterState): void {
window.localStorage.setItem(STATE_KEY, JSON.stringify(state));
}
@@ -3423,7 +3455,7 @@ function dedupeApprovalTimelineEvents(events: ApprovalTimelineResponse['events']
}
function notificationAdapterState(snapshot: QimingclawSnapshot | null): AdapterState {
return snapshot?.uiState ?? readState();
return snapshot?.uiState ? normalizeAdapterState(snapshot.uiState) : readState();
}
function buildNotificationsResponse(status: string | null, snapshot: QimingclawSnapshot | null): { notifications: UserNotification[] } {
@@ -3450,9 +3482,72 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
const overlays = state.notificationStateById ?? {};
return notifications
.map((notification) => applyNotificationOverlay(notification, overlays[notification.notification_id]))
.filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences))
.sort((left, right) => right.created_at.localeCompare(left.created_at) || right.notification_id.localeCompare(left.notification_id));
}
function defaultNotificationPreferences(): NotificationPreferences {
return {
enabled: true,
muted_kinds: [],
muted_levels: [],
updated_at: null,
};
}
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
const stored = state?.notificationPreferences;
const fallback = defaultNotificationPreferences();
if (!stored || typeof stored !== 'object') return fallback;
const mutedKinds = Array.isArray(stored.muted_kinds)
? stored.muted_kinds.filter((kind): kind is UserNotification['kind'] => NOTIFICATION_KINDS.includes(kind as UserNotification['kind']))
: [];
const mutedLevels = Array.isArray(stored.muted_levels)
? stored.muted_levels.filter((level): level is UserNotification['level'] => NOTIFICATION_LEVELS.includes(level as UserNotification['level']))
: [];
return {
enabled: stored.enabled !== false,
muted_kinds: [...new Set(mutedKinds)],
muted_levels: [...new Set(mutedLevels)],
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
};
}
function notificationMatchesPreferences(
notification: UserNotification,
preferences: NotificationPreferences | undefined,
): boolean {
const normalized = notificationPreferences({ notificationPreferences: preferences });
if (!normalized.enabled) return false;
if (normalized.muted_levels.includes(notification.level)) return false;
return !normalized.muted_kinds.includes(notification.kind);
}
async function handleNotificationPreferencesPatch(
patch: Partial<NotificationPreferences>,
snapshot: QimingclawSnapshot | null,
): Promise<NotificationPreferences> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = notificationPreferences(state);
const next = notificationPreferences({
notificationPreferences: {
...current,
...patch,
updated_at: new Date().toISOString(),
},
});
await saveState(
{
...normalizeAdapterState(state),
notificationPreferences: next,
},
'update_notification_preferences',
undefined,
{ notification_preferences: next },
);
return next;
}
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
return buildApprovals(snapshot)