吸收数字员工通知跨端同步

This commit is contained in:
baiyanyun
2026-06-11 20:27:10 +08:00
parent 4c862ef4b7
commit 8278fdc81c
15 changed files with 745 additions and 193 deletions

View File

@@ -19,6 +19,7 @@ import type {
PlanStepDispatchPolicyPullResult,
ReadyStepDispatchResult,
NotificationPreferences,
NotificationUpdatesPullResult,
UserNotification,
DebugStoreResponse,
DebugPlan,
@@ -567,7 +568,7 @@ export function getEventLog(params: {
// Notifications
// ---------------------------------------------------------------------------
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type PlanStepDispatchPolicy, type UserNotification };
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type NotificationUpdatesPullResult, type PlanStepDispatchPolicy, type UserNotification };
export function getNotifications(
status: string = 'unread',
@@ -596,6 +597,12 @@ export function patchNotificationPreferences(
});
}
export function pullNotificationUpdates(): Promise<NotificationUpdatesPullResult> {
return apiFetch<NotificationUpdatesPullResult>('/api/notifications/updates/pull', {
method: 'POST',
});
}
export function getApprovalSubscriptionPreferences(): Promise<ApprovalSubscriptionPreferences> {
return apiFetch<ApprovalSubscriptionPreferences>('/api/approval/subscriptions');
}

View File

@@ -64,6 +64,8 @@ declare global {
pullRiskApprovalPolicy?: () => Promise<{ ok?: boolean; skipped?: boolean; error?: string | null }>;
pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
submitApprovalDecision?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
pullNotificationUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
submitNotificationAction?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise<QimingclawRiskPolicyResult>;
pullRouteDecisionPolicy?: () => Promise<RouteDecisionPolicyPullResult>;
evaluateRouteDecisionPolicy?: (input: QimingclawRouteDecisionPolicyInput) => Promise<QimingclawRouteDecisionPolicyResult>;
@@ -1026,6 +1028,9 @@ export async function handleQimingclawDigitalApi<T>(
await readQimingclawSnapshot(),
) as T;
}
if (method === 'POST' && pathname === '/api/notifications/updates/pull') {
return await pullBridgeNotificationUpdates() 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;
@@ -1308,6 +1313,25 @@ async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: num
}
}
async function pullBridgeNotificationUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.pullNotificationUpdates) {
return { ok: false, error: 'qimingclaw_bridge_unavailable' };
}
try {
const result = await bridge.pullNotificationUpdates();
return {
ok: result?.ok !== false,
applied: Number(result?.applied ?? 0),
ignored: Number(result?.ignored ?? 0),
...(result?.skipped ? { skipped: true } : {}),
...(result?.error ? { error: result.error } : {}),
};
} catch (error) {
return { ok: 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) => (
@@ -4753,7 +4777,7 @@ async function handleNotificationAction(
action: 'read' | 'dismiss' | 'reply',
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<{ ok: boolean; task_id?: string; status?: string; task_input_recorded?: boolean; governance_command_id?: string; governance_event_id?: string }> {
): Promise<{ ok: boolean; task_id?: string; status?: string; notification_sync_error?: string | null; task_input_recorded?: boolean; governance_command_id?: string; governance_event_id?: string }> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = buildNotifications(snapshot, state).find((notification) => notification.notification_id === notificationId);
const now = new Date().toISOString();
@@ -4782,6 +4806,18 @@ async function handleNotificationAction(
},
};
await saveState(nextState, `notification_${action}`, undefined, { notification_id: notificationId, action });
const writeback = await submitBridgeNotificationAction({
notification_id: notificationId,
action,
status: overlay.status ?? current?.status ?? null,
read_at: overlay.read_at ?? null,
dismissed_at: overlay.dismissed_at ?? null,
reply: action === 'reply' ? replyContent : overlay.reply ?? null,
replied_at: overlay.replied_at ?? null,
acted_at: action === 'dismiss' ? overlay.dismissed_at ?? now : action === 'reply' ? overlay.replied_at ?? now : overlay.read_at ?? now,
actor: 'digital_employee_ui',
source: 'sgrobot-digital-adapter',
});
const taskInput = action === 'reply' && current?.kind === 'need_input' && current.task_id && replyContent && !alreadyReplied
? await recordNotificationTaskInput(current, replyContent, snapshot)
: null;
@@ -4789,12 +4825,24 @@ async function handleNotificationAction(
ok: true,
task_id: current?.task_id,
status: overlay.status,
...(writeback.ok ? {} : { notification_sync_error: writeback.error }),
task_input_recorded: Boolean(taskInput?.accepted),
governance_command_id: taskInput?.command_id,
governance_event_id: governanceEventIdFromCommand(taskInput),
};
}
async function submitBridgeNotificationAction(input: Record<string, unknown>): Promise<{ ok: boolean; error?: string | null }> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.submitNotificationAction) return { ok: false, error: 'qimingclaw_bridge_unavailable' };
try {
const result = await bridge.submitNotificationAction(input);
return { ok: result?.ok !== false, ...(result?.error ? { error: result.error } : {}) };
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) };
}
}
async function recordNotificationTaskInput(
notification: UserNotification,
replyContent: string,