吸收数字员工通知权限引导

This commit is contained in:
baiyanyun
2026-06-12 10:16:02 +08:00
parent e57c94c19d
commit e473b5c147
17 changed files with 642 additions and 204 deletions

View File

@@ -19,6 +19,9 @@ import type {
PlanStepDispatchPolicyPullResult,
ReadyStepDispatchResult,
RiskApprovalPolicy,
DesktopNotificationSettingsOpenResult,
DesktopNotificationShowResult,
DesktopNotificationStatus,
NotificationPreferences,
NotificationUpdatesPullResult,
UserNotification,
@@ -569,7 +572,7 @@ export function getEventLog(params: {
// Notifications
// ---------------------------------------------------------------------------
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type NotificationUpdatesPullResult, type PlanStepDispatchPolicy, type RiskApprovalPolicy, type UserNotification };
export { type ApprovalSubscriptionPreferences, type DesktopNotificationSettingsOpenResult, type DesktopNotificationShowResult, type DesktopNotificationStatus, type NotificationPreferences, type NotificationUpdatesPullResult, type PlanStepDispatchPolicy, type RiskApprovalPolicy, type UserNotification };
export function getNotifications(
status: string = 'unread',
@@ -604,13 +607,24 @@ export function pullNotificationUpdates(): Promise<NotificationUpdatesPullResult
});
}
export function showDesktopNotification(input: Pick<UserNotification, 'notification_id' | 'title' | 'body' | 'level' | 'kind' | 'status'>): Promise<{ ok: boolean; shown?: boolean; reason?: string | null; error?: string | null }> {
return apiFetch<{ ok: boolean; shown?: boolean; reason?: string | null; error?: string | null }>('/api/notifications/desktop', {
export function showDesktopNotification(input: Pick<UserNotification, 'notification_id' | 'title' | 'body' | 'level' | 'kind' | 'status'>): Promise<DesktopNotificationShowResult> {
return apiFetch<DesktopNotificationShowResult>('/api/notifications/desktop', {
method: 'POST',
body: JSON.stringify(input),
});
}
export function getDesktopNotificationStatus(): Promise<DesktopNotificationStatus> {
return apiFetch<DesktopNotificationStatus>('/api/notifications/desktop/status');
}
export function openDesktopNotificationSettings(): Promise<DesktopNotificationSettingsOpenResult> {
return apiFetch<DesktopNotificationSettingsOpenResult>('/api/notifications/desktop/settings/open', {
method: 'POST',
body: JSON.stringify({}),
});
}
export function getApprovalSubscriptionPreferences(): Promise<ApprovalSubscriptionPreferences> {
return apiFetch<ApprovalSubscriptionPreferences>('/api/approval/subscriptions');
}

View File

@@ -68,6 +68,8 @@ declare global {
pullNotificationUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
submitNotificationAction?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
showDesktopNotification?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; shown?: boolean; reason?: string | null; error?: string | null }>;
getDesktopNotificationStatus?: () => Promise<{ supported?: boolean; platform?: string; can_open_settings?: boolean; reason?: string | null }>;
openDesktopNotificationSettings?: () => Promise<{ success?: boolean; platform?: string; reason?: string | null; error?: string | null }>;
evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise<QimingclawRiskPolicyResult>;
pullRouteDecisionPolicy?: () => Promise<RouteDecisionPolicyPullResult>;
evaluateRouteDecisionPolicy?: (input: QimingclawRouteDecisionPolicyInput) => Promise<QimingclawRouteDecisionPolicyResult>;
@@ -1046,6 +1048,12 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'POST' && pathname === '/api/notifications/desktop') {
return await showBridgeDesktopNotification(parseOptionalJsonBody(options.body)) as T;
}
if (method === 'GET' && pathname === '/api/notifications/desktop/status') {
return await readBridgeDesktopNotificationStatus() as T;
}
if (method === 'POST' && pathname === '/api/notifications/desktop/settings/open') {
return await openBridgeDesktopNotificationSettings() 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;
@@ -1368,6 +1376,52 @@ async function showBridgeDesktopNotification(input: Record<string, unknown>): Pr
}
}
async function readBridgeDesktopNotificationStatus(): Promise<{ supported: boolean; platform: string; can_open_settings: boolean; reason?: string | null }> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.getDesktopNotificationStatus) {
return {
supported: false,
platform: 'unknown',
can_open_settings: false,
reason: 'qimingclaw_bridge_unavailable',
};
}
try {
const result = await bridge.getDesktopNotificationStatus();
return {
supported: result?.supported === true,
platform: stringValue(result?.platform) || 'unknown',
can_open_settings: result?.can_open_settings === true,
reason: stringValue(result?.reason) || null,
};
} catch (error) {
return {
supported: false,
platform: 'unknown',
can_open_settings: false,
reason: error instanceof Error ? error.message : String(error),
};
}
}
async function openBridgeDesktopNotificationSettings(): Promise<{ success: boolean; platform?: string | null; reason?: string | null; error?: string | null }> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.openDesktopNotificationSettings) {
return { success: false, reason: 'qimingclaw_bridge_unavailable' };
}
try {
const result = await bridge.openDesktopNotificationSettings();
return {
success: result?.success === true,
platform: stringValue(result?.platform) || null,
reason: stringValue(result?.reason) || null,
error: stringValue(result?.error) || null,
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
async function resolveApprovedRouteInterventions(snapshot: QimingclawSnapshot | null): Promise<void> {
const timeline = await readRouteInterventions(120);
const approved = timeline.interventions.filter((item) => (