feat(desktop): surface runtime alerts as system notifications

Add a runtime system notifier that pushes danger-severity activity (and
forced account-access alerts) to the OS via the tray balloon on Windows,
falling back to Electron Notification elsewhere, with a 30s per-key
dedupe window. Route risk-control / human-verification account alerts
through it so users are notified when an account needs attention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 22:01:38 +08:00
parent 1fab7b1e5f
commit 5c3f2c7822
5 changed files with 193 additions and 13 deletions
+31 -4
View File
@@ -1,6 +1,6 @@
import { nativeImage } from 'electron/common'
import type { Tray as ElectronTray } from 'electron/main'
import { Menu, Tray } from 'electron/main'
import { Menu, Notification, Tray } from 'electron/main'
import { TRAY_DANGER_ICON_BASE64, TRAY_ICON_BASE64, TRAY_WINDOWS_ICON_BASE64 } from './brand-icons'
@@ -52,12 +52,12 @@ function formatIssueCount(count: number): string {
// Windows-only: tray.displayBalloon shows a system toast next to the tray
// icon. macOS and Linux ignore this call (Electron returns silently), so the
// guard here is just to avoid the no-op log noise on non-Windows.
export function showTrayBalloon(title: string, content: string): void {
function displayTrayBalloon(title: string, content: string): boolean {
if (process.platform !== 'win32') {
return
return false
}
if (!tray || tray.isDestroyed()) {
return
return false
}
try {
tray.displayBalloon({
@@ -66,11 +66,38 @@ export function showTrayBalloon(title: string, content: string): void {
icon: createTrayIcon('normal'),
iconType: 'custom',
})
return true
} catch (error) {
console.warn('[desktop-tray] displayBalloon failed', error)
return false
}
}
export function showTrayBalloon(title: string, content: string): void {
displayTrayBalloon(title, content)
}
export function showSystemNotification(title: string, body: string): void {
const normalizedTitle = title.trim()
const normalizedBody = body.trim()
if (!normalizedTitle || !normalizedBody) {
return
}
if (displayTrayBalloon(normalizedTitle, normalizedBody)) {
return
}
if (!Notification.isSupported()) {
return
}
const notification = new Notification({
title: normalizedTitle,
body: normalizedBody,
silent: false,
})
notification.show()
}
export function updateTrayIssueIndicator(issueCount: number): void {
if (!tray || tray.isDestroyed()) {
return