吸收数字员工管理端通知视图

This commit is contained in:
baiyanyun
2026-06-10 17:05:57 +08:00
parent 5b2f14123e
commit 1271e4967b
7 changed files with 234 additions and 46 deletions

View File

@@ -687,7 +687,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'audits';
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'notifications' | 'audits';
type BusinessViewRow = Record<string, unknown>;
export function isQimingclawDigitalApiEnabled(): boolean {
@@ -771,7 +771,7 @@ export async function handleQimingclawDigitalApi<T>(
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions|audits)$/);
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions|notifications|audits)$/);
if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
}
@@ -2507,13 +2507,16 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
artifacts: businessArtifactRows(snapshot),
approvals: businessApprovalRows(snapshot),
'route-decisions': routeDecisionRows(snapshot),
notifications: notificationRows(snapshot, notificationAdapterState(snapshot)),
audits: auditRows(snapshot),
};
const rows = kind === 'route-decisions'
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
: kind === 'audits'
? filterAuditRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
: kind === 'notifications'
? filterNotificationRows(rowsByKind[kind], searchParams)
: kind === 'audits'
? filterAuditRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
return {
...paginateBusinessRows(rows, searchParams),
source: BUSINESS_VIEW_SOURCE,
@@ -2555,6 +2558,50 @@ function filterRouteDecisionRows(rows: BusinessViewRow[], searchParams: URLSearc
.filter((row) => !policyResult || stringValue(row.policy_result) === policyResult);
}
function notificationRows(snapshot: QimingclawSnapshot | null, state: AdapterState): BusinessViewRow[] {
const preferences = notificationPreferences(state);
return buildNotificationCandidates(snapshot, state)
.map((notification) => ({
notification_id: notification.notification_id,
kind: notification.kind,
level: notification.level,
status: notification.status,
title: notification.title,
body: notification.body,
plan_id: notification.plan_id,
task_id: notification.task_id,
correlation_id: notification.correlation_id,
created_at: notification.created_at,
updated_at: notification.replied_at || notification.dismissed_at || notification.read_at || notification.created_at,
read_at: notification.read_at ?? null,
dismissed_at: notification.dismissed_at ?? null,
replied_at: notification.replied_at ?? null,
muted_by_preferences: !notificationMatchesPreferences(notification, preferences),
entity_type: 'notification',
entity_id: notification.notification_id,
source: BUSINESS_VIEW_SOURCE,
}))
.sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)) || stringValue(right.notification_id).localeCompare(stringValue(left.notification_id)));
}
function filterNotificationRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const id = stringValue(searchParams.get('id'));
const kind = stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const muted = stringValue(searchParams.get('muted')).toLowerCase();
return filterBusinessRows(rows, searchParams)
.filter((row) => !id || stringValue(row.notification_id) === id || stringValue(row.correlation_id) === id || stringValue(row.entity_id) === id)
.filter((row) => !kind || stringValue(row.kind) === kind)
.filter((row) => !level || stringValue(row.level) === level)
.filter((row) => {
if (!muted) return true;
const mutedValue = row.muted_by_preferences === true;
if (['1', 'true', 'yes'].includes(muted)) return mutedValue;
if (['0', 'false', 'no'].includes(muted)) return !mutedValue;
return true;
});
}
function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event: QimingclawEventRecord): RouteDecisionRecord {
const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {};
return {
@@ -3185,6 +3232,7 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
row.event_id,
row.artifact_id,
row.approval_id,
row.notification_id,
row.report_id,
]);
}
@@ -3468,6 +3516,11 @@ function buildNotificationsResponse(status: string | null, snapshot: QimingclawS
}
function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
return buildNotificationCandidates(snapshot, state)
.filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences));
}
function buildNotificationCandidates(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
const notifications = dedupeNotifications([
...approvalNotifications(snapshot),
...approvalActionNotifications(snapshot),
@@ -3482,7 +3535,6 @@ 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));
}