From 8278fdc81c0e4283a28ef81e6efc9694f1989407 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Thu, 11 Jun 2026 20:27:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=B8=E6=94=B6=E6=95=B0=E5=AD=97=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E9=80=9A=E7=9F=A5=E8=B7=A8=E7=AB=AF=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../embedded/sgrobot-digital/src/lib/api.ts | 9 +- .../src/lib/qimingclawAdapter.ts | 50 +++- .../pages/digital/DigitalEmployeeShell.tsx | 23 +- .../embedded/sgrobot-digital/src/types/api.ts | 10 + .../sgrobot-digital/assets/index-BX3uWr8J.js | 184 +++++++++++++ .../sgrobot-digital/assets/index-CEkePwhY.js | 184 ------------- .../public/sgrobot-digital/index.html | 2 +- .../scripts/check-digital-employee.js | 25 ++ .../main/ipc/digitalEmployeeHandlers.test.ts | 26 ++ .../src/main/ipc/digitalEmployeeHandlers.ts | 10 + .../services/digitalEmployee/stateService.ts | 8 + .../digitalEmployee/syncService.test.ts | 151 +++++++++++ .../services/digitalEmployee/syncService.ts | 242 +++++++++++++++++- .../src/preload/webviewPerfBridge.ts | 6 + ...ital-employee-frontend-integration-plan.md | 8 +- 15 files changed, 745 insertions(+), 193 deletions(-) create mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-BX3uWr8J.js delete mode 100644 qimingclaw/crates/agent-electron-client/public/sgrobot-digital/assets/index-CEkePwhY.js diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts index fdd9dd69..2fe8036b 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts @@ -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 { + return apiFetch('/api/notifications/updates/pull', { + method: 'POST', + }); +} + export function getApprovalSubscriptionPreferences(): Promise { return apiFetch('/api/approval/subscriptions'); } diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts index ce0d3b38..89b82be8 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts @@ -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) => Promise<{ ok?: boolean; error?: string | null }>; + pullNotificationUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>; + submitNotificationAction?: (input: Record) => Promise<{ ok?: boolean; error?: string | null }>; evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise; pullRouteDecisionPolicy?: () => Promise; evaluateRouteDecisionPolicy?: (input: QimingclawRouteDecisionPolicyInput) => Promise; @@ -1026,6 +1028,9 @@ export async function handleQimingclawDigitalApi( 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 { const timeline = await readRouteInterventions(120); const approved = timeline.interventions.filter((item) => ( @@ -4753,7 +4777,7 @@ async function handleNotificationAction( action: 'read' | 'dismiss' | 'reply', body: Record, 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): 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, diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/DigitalEmployeeShell.tsx b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/DigitalEmployeeShell.tsx index ee24b4fb..947b4bf4 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/DigitalEmployeeShell.tsx +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/DigitalEmployeeShell.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { Bell, Check, Mail, MessageSquare, Settings2, X } from 'lucide-react'; +import { Bell, Check, Mail, MessageSquare, RefreshCw, Settings2, X } from 'lucide-react'; import { useLocation, useNavigate } from 'react-router-dom'; import { RoleProvider } from '@/contexts/RoleContext'; import { @@ -9,6 +9,7 @@ import { getUnreadNotificationCount, markNotificationRead, patchNotificationPreferences, + pullNotificationUpdates, replyToNotification, type NotificationPreferences, type UserNotification, @@ -129,6 +130,7 @@ export default function DigitalEmployeeShell() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [notificationsLoading, setNotificationsLoading] = useState(false); + const [notificationsSyncing, setNotificationsSyncing] = useState(false); const [notificationSettingsOpen, setNotificationSettingsOpen] = useState(false); const [notificationPreferences, setNotificationPreferences] = useState(DEFAULT_NOTIFICATION_PREFERENCES); const [replyDraftById, setReplyDraftById] = useState>({}); @@ -195,6 +197,16 @@ export default function DigitalEmployeeShell() { await refreshNotifications(); }, [refreshNotifications, replyDraftById]); + const handleNotificationSync = useCallback(async () => { + setNotificationsSyncing(true); + try { + await pullNotificationUpdates(); + } finally { + setNotificationsSyncing(false); + await refreshNotifications(); + } + }, [refreshNotifications]); + const updateNotificationPreferences = useCallback(async (patch: Partial) => { const next = await patchNotificationPreferences(patch); setNotificationPreferences(next); @@ -262,6 +274,15 @@ export default function DigitalEmployeeShell() { {unreadCount > 0 ? `${unreadCount} 条未读` : '暂无未读'}
+