mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: restore Windows desktop notifications
This commit is contained in:
parent
dfb69976fd
commit
a0624eeb21
@ -830,6 +830,26 @@ async fn macos_send_notification(
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_windows_notification_settings() -> Result<bool, String> {
|
||||
open_windows_notification_settings_impl()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn open_windows_notification_settings_impl() -> Result<bool, String> {
|
||||
StdCommand::new("explorer.exe")
|
||||
.arg("ms-settings:notifications")
|
||||
.spawn()
|
||||
.map_err(|err| format!("open Windows notification settings: {err}"))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn open_windows_notification_settings_impl() -> Result<bool, String> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn run_notification_bridge<T, F>(operation: F) -> Result<T, String>
|
||||
where
|
||||
T: Send + 'static,
|
||||
@ -1533,7 +1553,8 @@ pub fn run() {
|
||||
terminal_kill,
|
||||
macos_notification_permission_state,
|
||||
macos_request_notification_permission,
|
||||
macos_send_notification
|
||||
macos_send_notification,
|
||||
open_windows_notification_settings
|
||||
]);
|
||||
|
||||
// macOS: native menu bar (traffic-light overlay style)
|
||||
|
||||
@ -218,7 +218,7 @@ describe('Settings > General tab', () => {
|
||||
})
|
||||
expect(desktopNotificationsMock.notifyDesktop).toHaveBeenCalledWith({
|
||||
title: 'Claude Code Haha notifications are enabled',
|
||||
body: 'Permission prompts and completed agent replies will now use macOS notifications.',
|
||||
body: 'Permission prompts and completed agent replies will now use system notifications.',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -655,7 +655,7 @@ export const en = {
|
||||
'settings.general.notificationsAuthorize': 'Authorize',
|
||||
'settings.general.notificationsOpenSettings': 'Open Settings',
|
||||
'settings.general.notificationsTestTitle': 'Claude Code Haha notifications are enabled',
|
||||
'settings.general.notificationsTestBody': 'Permission prompts and completed agent replies will now use macOS notifications.',
|
||||
'settings.general.notificationsTestBody': 'Permission prompts and completed agent replies will now use system notifications.',
|
||||
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
|
||||
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
|
||||
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',
|
||||
|
||||
@ -657,7 +657,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.notificationsAuthorize': '授权通知',
|
||||
'settings.general.notificationsOpenSettings': '打开系统设置',
|
||||
'settings.general.notificationsTestTitle': 'Claude Code Haha 通知已启用',
|
||||
'settings.general.notificationsTestBody': '后续授权确认和 Agent 回复完成都会通过 macOS 系统通知提醒。',
|
||||
'settings.general.notificationsTestBody': '后续授权确认和 Agent 回复完成都会通过系统通知提醒。',
|
||||
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
|
||||
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
|
||||
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',
|
||||
|
||||
@ -12,6 +12,9 @@ const coreApiMock = vi.hoisted(() => ({
|
||||
const eventApiMock = vi.hoisted(() => ({
|
||||
listen: vi.fn(),
|
||||
}))
|
||||
const shellApiMock = vi.hoisted(() => ({
|
||||
open: vi.fn(),
|
||||
}))
|
||||
const requestUserAttentionMock = vi.hoisted(() => vi.fn())
|
||||
const windowApiMock = vi.hoisted(() => ({
|
||||
requestUserAttention: requestUserAttentionMock,
|
||||
@ -28,11 +31,13 @@ vi.mock('@tauri-apps/plugin-notification', () => notificationPluginMock)
|
||||
vi.mock('@tauri-apps/api/core', () => coreApiMock)
|
||||
vi.mock('@tauri-apps/api/event', () => eventApiMock)
|
||||
vi.mock('@tauri-apps/api/window', () => windowApiMock)
|
||||
vi.mock('@tauri-apps/plugin-shell', () => shellApiMock)
|
||||
|
||||
import {
|
||||
getDesktopNotificationPermission,
|
||||
installDesktopNotificationClickListener,
|
||||
notifyDesktop,
|
||||
openDesktopNotificationSettings,
|
||||
requestDesktopNotificationPermission,
|
||||
resetDesktopNotificationsForTests,
|
||||
setNativeNotificationSenderForTests,
|
||||
@ -45,6 +50,7 @@ describe('desktopNotifications', () => {
|
||||
resetDesktopNotificationsForTests()
|
||||
coreApiMock.invoke.mockReset()
|
||||
eventApiMock.listen.mockReset()
|
||||
shellApiMock.open.mockReset()
|
||||
notificationPluginMock.isPermissionGranted.mockReset()
|
||||
notificationPluginMock.requestPermission.mockReset()
|
||||
notificationPluginMock.sendNotification.mockReset()
|
||||
@ -231,6 +237,57 @@ describe('desktopNotifications', () => {
|
||||
expect(notificationPluginMock.requestPermission).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reads and requests Windows notification permission through the native plugin command', async () => {
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
value: 'Win32',
|
||||
})
|
||||
coreApiMock.invoke
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockResolvedValueOnce('granted')
|
||||
|
||||
await expect(getDesktopNotificationPermission()).resolves.toBe('granted')
|
||||
await expect(requestDesktopNotificationPermission()).resolves.toBe('granted')
|
||||
|
||||
expect(coreApiMock.invoke).toHaveBeenNthCalledWith(1, 'plugin:notification|is_permission_granted')
|
||||
expect(coreApiMock.invoke).toHaveBeenNthCalledWith(2, 'plugin:notification|request_permission')
|
||||
expect(notificationPluginMock.isPermissionGranted).not.toHaveBeenCalled()
|
||||
expect(notificationPluginMock.requestPermission).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends Windows notifications when the native plugin reports permission granted', async () => {
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
value: 'Win32',
|
||||
})
|
||||
coreApiMock.invoke.mockResolvedValueOnce(true)
|
||||
|
||||
await expect(notifyDesktop({
|
||||
title: 'Permission required',
|
||||
body: 'Approve command execution',
|
||||
})).resolves.toBe(true)
|
||||
|
||||
expect(coreApiMock.invoke).toHaveBeenCalledWith('plugin:notification|is_permission_granted')
|
||||
expect(notificationPluginMock.isPermissionGranted).not.toHaveBeenCalled()
|
||||
expect(notificationPluginMock.sendNotification).toHaveBeenCalledWith({
|
||||
title: 'Permission required',
|
||||
body: 'Approve command execution',
|
||||
})
|
||||
})
|
||||
|
||||
it('opens Windows notification settings through the native command', async () => {
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
value: 'Win32',
|
||||
})
|
||||
coreApiMock.invoke.mockResolvedValueOnce(true)
|
||||
|
||||
await expect(openDesktopNotificationSettings()).resolves.toBe(true)
|
||||
|
||||
expect(coreApiMock.invoke).toHaveBeenCalledWith('open_windows_notification_settings')
|
||||
expect(shellApiMock.open).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports and requests macOS notification permission through the native bridge', async () => {
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
|
||||
@ -26,6 +26,7 @@ type NativeNotificationPayload = {
|
||||
|
||||
type NativeNotificationSender = (options: NativeNotificationPayload) => Promise<boolean> | boolean
|
||||
export type DesktopNotificationPermission = NotificationPermission | 'unsupported'
|
||||
type PluginPermissionState = DesktopNotificationPermission | 'prompt' | 'prompt-with-rationale'
|
||||
|
||||
const TARGET_EXTRA_KEY = 'ccHahaTarget'
|
||||
const notifiedKeys = new Set<string>()
|
||||
@ -46,10 +47,12 @@ function readBrowserNotificationPermission(): DesktopNotificationPermission {
|
||||
function detectPlatform(): 'darwin' | 'win32' | 'linux' | 'unknown' {
|
||||
const platform = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase() : ''
|
||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''
|
||||
const raw = `${platform} ${userAgent}`
|
||||
if (raw.includes('mac')) return 'darwin'
|
||||
if (raw.includes('win')) return 'win32'
|
||||
if (raw.includes('linux')) return 'linux'
|
||||
if (platform.includes('mac')) return 'darwin'
|
||||
if (platform.includes('win')) return 'win32'
|
||||
if (platform.includes('linux')) return 'linux'
|
||||
if (userAgent.includes('mac')) return 'darwin'
|
||||
if (userAgent.includes('win')) return 'win32'
|
||||
if (userAgent.includes('linux')) return 'linux'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
@ -64,6 +67,46 @@ function getNotificationSettingsUrl(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePermission(value: unknown): DesktopNotificationPermission {
|
||||
if (value === true) return 'granted'
|
||||
if (value === false) return 'denied'
|
||||
if (value === null) return 'default'
|
||||
if (value === 'prompt' || value === 'prompt-with-rationale') return 'default'
|
||||
return ['default', 'denied', 'granted', 'unsupported'].includes(value as string)
|
||||
? value as DesktopNotificationPermission
|
||||
: 'unsupported'
|
||||
}
|
||||
|
||||
async function invokeWindowsNotificationPermissionState(): Promise<DesktopNotificationPermission | null> {
|
||||
if (detectPlatform() !== 'win32') return null
|
||||
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const granted = await invoke<boolean | null>('plugin:notification|is_permission_granted')
|
||||
return normalizePermission(granted)
|
||||
} catch (err) {
|
||||
if (typeof console !== 'undefined') {
|
||||
console.warn('[desktopNotifications] failed to read Windows notification permission:', err)
|
||||
}
|
||||
return 'unsupported'
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeWindowsNotificationPermissionRequest(): Promise<DesktopNotificationPermission | null> {
|
||||
if (detectPlatform() !== 'win32') return null
|
||||
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const permission = await invoke<PluginPermissionState>('plugin:notification|request_permission')
|
||||
return normalizePermission(permission)
|
||||
} catch (err) {
|
||||
if (typeof console !== 'undefined') {
|
||||
console.warn('[desktopNotifications] failed to request Windows notification permission:', err)
|
||||
}
|
||||
return 'unsupported'
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeMacNotificationPermissionState(): Promise<DesktopNotificationPermission | null> {
|
||||
if (detectPlatform() !== 'darwin') return null
|
||||
|
||||
@ -187,6 +230,9 @@ export async function getDesktopNotificationPermission(): Promise<DesktopNotific
|
||||
const macPermission = await invokeMacNotificationPermissionState()
|
||||
if (macPermission) return macPermission
|
||||
|
||||
const windowsPermission = await invokeWindowsNotificationPermissionState()
|
||||
if (windowsPermission) return windowsPermission
|
||||
|
||||
try {
|
||||
const { isPermissionGranted } = await import('@tauri-apps/plugin-notification')
|
||||
if (await isPermissionGranted()) return 'granted'
|
||||
@ -200,6 +246,9 @@ export async function requestDesktopNotificationPermission(): Promise<DesktopNot
|
||||
const macPermission = await invokeMacNotificationPermissionRequest()
|
||||
if (macPermission) return macPermission
|
||||
|
||||
const windowsPermission = await invokeWindowsNotificationPermissionRequest()
|
||||
if (windowsPermission) return windowsPermission
|
||||
|
||||
try {
|
||||
const {
|
||||
isPermissionGranted,
|
||||
@ -217,6 +266,16 @@ export async function openDesktopNotificationSettings(): Promise<boolean> {
|
||||
const url = getNotificationSettingsUrl()
|
||||
if (!url) return false
|
||||
|
||||
if (detectPlatform() === 'win32') {
|
||||
try {
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const opened = await invoke<boolean>('open_windows_notification_settings')
|
||||
if (opened) return true
|
||||
} catch {
|
||||
// Fall back to shell.open/window.open below.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { open } = await import('@tauri-apps/plugin-shell')
|
||||
await open(url)
|
||||
@ -240,7 +299,9 @@ async function sendNativeNotification(options: { title: string; body?: string; t
|
||||
sendNotification,
|
||||
} = await import('@tauri-apps/plugin-notification')
|
||||
|
||||
if (!(await isPermissionGranted())) {
|
||||
const windowsPermission = await invokeWindowsNotificationPermissionState()
|
||||
const permissionGranted = windowsPermission ? windowsPermission === 'granted' : await isPermissionGranted()
|
||||
if (!permissionGranted) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user