mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 03:10:08 +08:00
feat/notification (#6144)
* WIP * feat: integrate notification system using Electron's Notification API - Replaced the previous notification implementation with Electron's Notification API for better integration. - Updated notification types and structures to support new features. - Added translations for notification messages in multiple languages. - Cleaned up unused dependencies related to notifications. * refactor: remove unused node-notifier dependency from Electron config * clean: remove node-notifier from asarUnpack in Electron config * fix: update notification type in preload script to align with new structure * feat: Integrate NotificationService for user notifications across various components - Implemented NotificationService in useUpdateHandler to notify users of available updates. - Enhanced KnowledgeQueue to send success and error notifications during item processing. - Updated BackupService to notify users upon successful restoration and backup completion. - Added error notifications in message handling to improve user feedback on assistant responses. This update improves user experience by providing timely notifications for important actions and events. * feat: Refactor notification handling and integrate new notification settings - Moved SYSTEM_MODELS to a new file for better organization. - Enhanced notification handling across various components, including BackupService and KnowledgeQueue, to include a source attribute for better context. - Introduced notification settings in the GeneralSettings component, allowing users to toggle notifications for assistant messages, backups, and knowledge embedding. - Updated the Redux store to manage notification settings and added migration logic for the new settings structure. - Improved user feedback by ensuring notifications are sent based on user preferences. This update enhances the user experience by providing customizable notification options and clearer context for notifications. * feat: Add 'update' source to NotificationSource type for enhanced notification context * feat: Integrate electron-notification-state for improved notification handling - Added electron-notification-state package to manage Do Not Disturb settings. - Updated NotificationService to respect user DND preferences when sending notifications. - Adjusted notification settings in various components (BackupService, KnowledgeQueue, useUpdateHandler) to ensure notifications are sent based on user preferences. - Enhanced user feedback by allowing notifications to be silenced based on DND status. * feat: Add notification icon to Electron notifications * fix: import SYSTEM_MODELS in EditModelsPopup for improved model handling * feat(i18n): add knowledge base notifications in multiple languages - Added success and error messages for knowledge base operations in English, Japanese, Russian, Chinese (Simplified and Traditional). - Updated the KnowledgeQueue to utilize the new notification messages for better user feedback. * feat(notification): introduce NotificationProvider and integrate into App component - Added NotificationProvider to manage notifications within the application. - Updated App component to include NotificationProvider, enhancing user feedback through notifications. - Refactored NotificationQueue to support multiple listeners for notification handling.
This commit is contained in:
parent
374e97b9ab
commit
8e5f2a523d
@ -80,6 +80,7 @@
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-notification-state": "^1.0.4",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "6.6.4",
|
||||
"electron-window-state": "^5.0.3",
|
||||
|
||||
@ -21,6 +21,9 @@ export enum IpcChannel {
|
||||
App_InstallUvBinary = 'app:install-uv-binary',
|
||||
App_InstallBunBinary = 'app:install-bun-binary',
|
||||
|
||||
App_Notification = 'app:notification',
|
||||
App_OnNotificationClick = 'app:on-notification-click',
|
||||
|
||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||
|
||||
// Open
|
||||
|
||||
@ -8,6 +8,7 @@ import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
@ -20,6 +21,7 @@ import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import { getMcpInstance } from './services/MCPService'
|
||||
import NotificationService from './services/NotificationService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
@ -41,6 +43,7 @@ const obsidianVaultService = new ObsidianVaultService()
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater(mainWindow)
|
||||
const notificationService = new NotificationService(mainWindow)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Info, () => ({
|
||||
version: app.getVersion(),
|
||||
@ -200,6 +203,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
await appUpdater.checkForUpdates()
|
||||
})
|
||||
|
||||
// notification
|
||||
ipcMain.handle(IpcChannel.App_Notification, async (_, notification: Notification) => {
|
||||
await notificationService.sendNotification(notification)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.App_OnNotificationClick, (_, notification: Notification) => {
|
||||
mainWindow.webContents.send('notification-click', notification)
|
||||
})
|
||||
|
||||
// zip
|
||||
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
|
||||
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
|
||||
|
||||
35
src/main/services/NotificationService.ts
Normal file
35
src/main/services/NotificationService.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { BrowserWindow, Notification as ElectronNotification } from 'electron'
|
||||
import { getDoNotDisturb } from 'electron-notification-state'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
|
||||
class NotificationService {
|
||||
private window: BrowserWindow
|
||||
|
||||
constructor(window: BrowserWindow) {
|
||||
// Initialize the service
|
||||
this.window = window
|
||||
}
|
||||
|
||||
public async sendNotification(notification: Notification) {
|
||||
const shouldSilent = getDoNotDisturb() ?? !!notification.silent
|
||||
|
||||
// 使用 Electron Notification API
|
||||
const electronNotification = new ElectronNotification({
|
||||
title: notification.title,
|
||||
body: notification.message,
|
||||
silent: shouldSilent,
|
||||
icon: icon
|
||||
})
|
||||
|
||||
electronNotification.on('click', () => {
|
||||
this.window.show()
|
||||
this.window.webContents.send('notification-click', notification)
|
||||
})
|
||||
|
||||
electronNotification.show()
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService
|
||||
@ -3,6 +3,7 @@ import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
import { CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@ -25,6 +26,18 @@ const api = {
|
||||
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
|
||||
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
|
||||
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
|
||||
notification: {
|
||||
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.App_Notification, notification),
|
||||
onClick: (callback: () => void) => {
|
||||
const listener = () => {
|
||||
callback()
|
||||
}
|
||||
ipcRenderer.on(IpcChannel.App_OnNotificationClick, listener)
|
||||
return () => {
|
||||
ipcRenderer.off(IpcChannel.App_OnNotificationClick, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
system: {
|
||||
getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType),
|
||||
getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname)
|
||||
|
||||
@ -9,6 +9,7 @@ import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
@ -27,26 +28,28 @@ function App(): React.ReactElement {
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
|
||||
@ -441,7 +441,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
|
||||
{ id: 'deepseek-r1', name: 'DeepSeek-R1', provider: 'burncloud', group: 'deepseek-ai' },
|
||||
{ id: 'deepseek-v3', name: 'DeepSeek-V3', provider: 'burncloud', group: 'deepseek-ai' }
|
||||
|
||||
],
|
||||
|
||||
o3: [
|
||||
|
||||
75
src/renderer/src/context/NotificationProvider.tsx
Normal file
75
src/renderer/src/context/NotificationProvider.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { NotificationQueue } from '@renderer/queue/NotificationQueue'
|
||||
import { Notification } from '@renderer/types/notification'
|
||||
import { isFocused } from '@renderer/utils/window'
|
||||
import { notification } from 'antd'
|
||||
import React, { createContext, use, useEffect, useMemo } from 'react'
|
||||
|
||||
type NotificationContextType = {
|
||||
open: typeof notification.open
|
||||
destroy: typeof notification.destroy
|
||||
}
|
||||
|
||||
const typeMap: Record<string, 'info' | 'success' | 'warning' | 'error'> = {
|
||||
error: 'error',
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
info: 'info',
|
||||
progress: 'info',
|
||||
action: 'info'
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType | undefined>(undefined)
|
||||
|
||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [api, contextHolder] = notification.useNotification({
|
||||
stack: {
|
||||
threshold: 3
|
||||
},
|
||||
showProgress: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const queue = NotificationQueue.getInstance()
|
||||
const listener = async (notification: Notification) => {
|
||||
// 判断是否需要系统通知
|
||||
if (notification.channel === 'system' || !isFocused()) {
|
||||
window.api.notification.send(notification)
|
||||
return
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
api.open({
|
||||
message: notification.title,
|
||||
description: notification.message,
|
||||
duration: 3,
|
||||
placement: 'topRight',
|
||||
type: typeMap[notification.type] || 'info',
|
||||
key: notification.id,
|
||||
onClose: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
queue.subscribe(listener)
|
||||
return () => queue.unsubscribe(listener)
|
||||
}, [api])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
open: api.open,
|
||||
destroy: api.destroy
|
||||
}),
|
||||
[api]
|
||||
)
|
||||
|
||||
return (
|
||||
<NotificationContext value={value}>
|
||||
{contextHolder}
|
||||
{children}
|
||||
</NotificationContext>
|
||||
)
|
||||
}
|
||||
|
||||
export const useNotification = () => {
|
||||
const ctx = use(NotificationContext)
|
||||
if (!ctx) throw new Error('useNotification must be used within a NotificationProvider')
|
||||
return ctx
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
import { NotificationService } from '@renderer/services/NotificationService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import { useEffect } from 'react'
|
||||
@ -8,6 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
export default function useUpdateHandler() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const notificationService = NotificationService.getInstance()
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron) return
|
||||
@ -22,6 +25,14 @@ export default function useUpdateHandler() {
|
||||
}
|
||||
}),
|
||||
ipcRenderer.on(IpcChannel.UpdateAvailable, (_, releaseInfo: UpdateInfo) => {
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'info',
|
||||
title: t('button.update_available'),
|
||||
message: t('button.update_available', { version: releaseInfo.version }),
|
||||
timestamp: Date.now(),
|
||||
source: 'update'
|
||||
})
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
|
||||
@ -780,6 +780,11 @@
|
||||
"hide_sidebar": "Hide Sidebar",
|
||||
"show_sidebar": "Show Sidebar"
|
||||
},
|
||||
"notification": {
|
||||
"assistant": "Assistant Response",
|
||||
"knowledge.success": "Successfully added {{type}} to the knowledge base",
|
||||
"knowledge.error": "Failed to add {{type}} to knowledge base: {{error}}"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
"keep_alive_time.placeholder": "Minutes",
|
||||
@ -1494,6 +1499,12 @@
|
||||
"moresetting.check.confirm": "Confirm Selection",
|
||||
"moresetting.check.warn": "Please be cautious when selecting this option. Incorrect selection may cause the model to malfunction!",
|
||||
"moresetting.warn": "Risk Warning",
|
||||
"notification": {
|
||||
"title": "Notification Settings",
|
||||
"assistant": "Assistant Message",
|
||||
"backup": "Backup Message",
|
||||
"knowledge_embed": "KnowledgeBase Message"
|
||||
},
|
||||
"provider": {
|
||||
"add.name": "Provider Name",
|
||||
"add.name.placeholder": "Example: OpenAI",
|
||||
|
||||
@ -781,6 +781,11 @@
|
||||
"hide_sidebar": "サイドバーを非表示",
|
||||
"show_sidebar": "サイドバーを表示"
|
||||
},
|
||||
"notification": {
|
||||
"assistant": "助手回應",
|
||||
"knowledge.success": "ナレッジベースに{{type}}を正常に追加しました",
|
||||
"knowledge.error": "ナレッジベースへの{{type}}の追加に失敗しました: {{error}}"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
"keep_alive_time.placeholder": "分",
|
||||
@ -1707,6 +1712,12 @@
|
||||
"service_tier.auto": "自動",
|
||||
"service_tier.default": "デフォルト",
|
||||
"service_tier.flex": "フレックス"
|
||||
},
|
||||
"notification": {
|
||||
"title": "通知設定",
|
||||
"assistant": "アシスタントメッセージ",
|
||||
"backup": "バックアップメッセージ",
|
||||
"knowledge_embed": "ナレッジベースメッセージ"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
|
||||
@ -780,6 +780,11 @@
|
||||
"hide_sidebar": "Скрыть боковую панель",
|
||||
"show_sidebar": "Показать боковую панель"
|
||||
},
|
||||
"notification": {
|
||||
"assistant": "Ответ ассистента",
|
||||
"knowledge.success": "Успешно добавлено {{type}} в базу знаний",
|
||||
"knowledge.error": "Не удалось добавить {{type}} в базу знаний: {{error}}"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
"keep_alive_time.placeholder": "Минуты",
|
||||
@ -1707,7 +1712,13 @@
|
||||
"service_tier.flex": "Гибкий"
|
||||
},
|
||||
"about.debug.title": "Отладка",
|
||||
"about.debug.open": "Открыть"
|
||||
"about.debug.open": "Открыть",
|
||||
"notification": {
|
||||
"title": "Настройки уведомлений",
|
||||
"assistant": "Сообщение ассистента",
|
||||
"backup": "Резервное сообщение",
|
||||
"knowledge_embed": "Сообщение базы знаний"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
|
||||
@ -780,6 +780,11 @@
|
||||
"hide_sidebar": "隐藏侧边栏",
|
||||
"show_sidebar": "显示侧边栏"
|
||||
},
|
||||
"notification": {
|
||||
"assistant": "助手响应",
|
||||
"knowledge.success": "成功添加 {{type}} 到知识库",
|
||||
"knowledge.error": "添加 {{type}} 到知识库失败: {{error}}"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||
"keep_alive_time.placeholder": "分钟",
|
||||
@ -1494,6 +1499,12 @@
|
||||
"moresetting.check.confirm": "确认勾选",
|
||||
"moresetting.check.warn": "请慎重勾选此选项,勾选错误会导致模型无法正常使用!!!",
|
||||
"moresetting.warn": "风险警告",
|
||||
"notification": {
|
||||
"title": "通知设置",
|
||||
"assistant": "助手消息",
|
||||
"backup": "备份",
|
||||
"knowledge_embed": "知识嵌入"
|
||||
},
|
||||
"provider": {
|
||||
"add.name": "提供商名称",
|
||||
"add.name.placeholder": "例如 OpenAI",
|
||||
|
||||
@ -781,6 +781,11 @@
|
||||
"hide_sidebar": "隱藏側邊欄",
|
||||
"show_sidebar": "顯示側邊欄"
|
||||
},
|
||||
"notification": {
|
||||
"assistant": "助手回應",
|
||||
"knowledge.success": "成功將{{type}}新增至知識庫",
|
||||
"knowledge.error": "無法將 {{type}} 加入知識庫: {{error}}"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||
"keep_alive_time.placeholder": "分鐘",
|
||||
@ -1707,6 +1712,12 @@
|
||||
"service_tier.auto": "自動",
|
||||
"service_tier.default": "預設",
|
||||
"service_tier.flex": "彈性"
|
||||
},
|
||||
"notification": {
|
||||
"title": "通知設定",
|
||||
"assistant": "助手訊息",
|
||||
"backup": "備份訊息",
|
||||
"knowledge_embed": "知識庫訊息"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setEnableDataCollection, setLanguage } from '@renderer/store/settings'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setEnableDataCollection, setLanguage, setNotificationSettings } from '@renderer/store/settings'
|
||||
import { setProxyMode, setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import { LanguageVarious } from '@renderer/types'
|
||||
import { NotificationSource } from '@renderer/types/notification'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
import { Input, Select, Space, Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
@ -107,6 +109,12 @@ const GeneralSettings: FC = () => {
|
||||
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' }
|
||||
]
|
||||
|
||||
const notificationSettings = useSelector((state: RootState) => state.settings.notification)
|
||||
|
||||
const handleNotificationChange = (type: NotificationSource, value: boolean) => {
|
||||
dispatch(setNotificationSettings({ ...notificationSettings, [type]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer theme={themeMode}>
|
||||
<SettingGroup theme={theme}>
|
||||
@ -154,6 +162,27 @@ const GeneralSettings: FC = () => {
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.notification.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.notification.assistant')}</SettingRowTitle>
|
||||
<Switch checked={notificationSettings.assistant} onChange={(v) => handleNotificationChange('assistant', v)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.notification.backup')}</SettingRowTitle>
|
||||
<Switch checked={notificationSettings.backup} onChange={(v) => handleNotificationChange('backup', v)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.notification.knowledge_embed')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={notificationSettings.knowledgeEmbed}
|
||||
onChange={(v) => handleNotificationChange('knowledgeEmbed', v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.launch.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import Logger from '@renderer/config/logger'
|
||||
import db from '@renderer/databases'
|
||||
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||
import { NotificationService } from '@renderer/services/NotificationService'
|
||||
import store from '@renderer/store'
|
||||
import { clearCompletedProcessing, updateBaseItemUniqueId, updateItemProcessingStatus } from '@renderer/store/knowledge'
|
||||
import { KnowledgeItem } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import { t } from 'i18next'
|
||||
|
||||
class KnowledgeQueue {
|
||||
private processing: Map<string, boolean> = new Map()
|
||||
@ -84,6 +87,7 @@ class KnowledgeQueue {
|
||||
}
|
||||
|
||||
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
|
||||
const notificationService = NotificationService.getInstance()
|
||||
try {
|
||||
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
|
||||
Logger.log(`[KnowledgeQueue] Item ${item.id} has reached max retries, skipping`)
|
||||
@ -134,6 +138,16 @@ class KnowledgeQueue {
|
||||
|
||||
Logger.log(`[KnowledgeQueue] Successfully completed processing item ${item.id}`)
|
||||
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'success',
|
||||
title: t('knowledge.status_completed"'),
|
||||
message: t('notification.knowledge.success', { type: item.type }),
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'knowledgeEmbed'
|
||||
})
|
||||
|
||||
store.dispatch(
|
||||
updateItemProcessingStatus({
|
||||
baseId,
|
||||
@ -157,6 +171,19 @@ class KnowledgeQueue {
|
||||
store.dispatch(clearCompletedProcessing({ baseId }))
|
||||
} catch (error) {
|
||||
Logger.error(`[KnowledgeQueue] Error processing item ${item.id}: `, error)
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'error',
|
||||
title: t('common.knowledge'),
|
||||
message: t('notification.knowledge.error', {
|
||||
type: item.type,
|
||||
error: error instanceof Error ? error.message : 'Unkown error'
|
||||
}),
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'knowledgeEmbed'
|
||||
})
|
||||
|
||||
store.dispatch(
|
||||
updateItemProcessingStatus({
|
||||
baseId,
|
||||
|
||||
53
src/renderer/src/queue/NotificationQueue.ts
Normal file
53
src/renderer/src/queue/NotificationQueue.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { Notification } from '@renderer/types/notification'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
type NotificationListener = (notification: Notification) => Promise<void> | void
|
||||
|
||||
export class NotificationQueue {
|
||||
private static instance: NotificationQueue
|
||||
private queue = new PQueue({ concurrency: 1 })
|
||||
private listeners: NotificationListener[] = []
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): NotificationQueue {
|
||||
if (!NotificationQueue.instance) {
|
||||
NotificationQueue.instance = new NotificationQueue()
|
||||
}
|
||||
return NotificationQueue.instance
|
||||
}
|
||||
|
||||
public subscribe(listener: NotificationListener) {
|
||||
this.listeners.push(listener)
|
||||
}
|
||||
|
||||
public unsubscribe(listener: NotificationListener) {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
||||
}
|
||||
|
||||
public async add(notification: Notification): Promise<void> {
|
||||
await this.queue.add(() => Promise.all(this.listeners.map((listener) => listener(notification))))
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空通知队列
|
||||
*/
|
||||
public clear(): void {
|
||||
this.queue.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列中等待的任务数量
|
||||
*/
|
||||
public get pending(): number {
|
||||
return this.queue.pending
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列的大小(包括正在进行和等待的任务)
|
||||
*/
|
||||
public get size(): number {
|
||||
return this.queue.size
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,14 @@
|
||||
/* eslint-disable simple-import-sort/imports */
|
||||
import Logger from '@renderer/config/logger'
|
||||
import db from '@renderer/databases'
|
||||
import { upgradeToV7 } from '@renderer/databases/upgrades'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setWebDAVSyncState } from '@renderer/store/backup'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { NotificationService } from './NotificationService'
|
||||
|
||||
export async function backup(skipBackupFile: boolean) {
|
||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
||||
const fileContnet = await getBackupData()
|
||||
@ -18,6 +20,7 @@ export async function backup(skipBackupFile: boolean) {
|
||||
}
|
||||
|
||||
export async function restore() {
|
||||
const notificationService = NotificationService.getInstance()
|
||||
const file = await window.api.file.open({ filters: [{ name: '备份文件', extensions: ['bak', 'zip'] }] })
|
||||
|
||||
if (file) {
|
||||
@ -33,6 +36,15 @@ export async function restore() {
|
||||
}
|
||||
|
||||
await handleData(data)
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'success',
|
||||
title: i18n.t('common.success'),
|
||||
message: i18n.t('message.restore.success'),
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'backup'
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error('[Backup] restore: Error restoring backup file:', error)
|
||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||
@ -71,6 +83,7 @@ export async function backupToWebdav({
|
||||
customFileName = '',
|
||||
autoBackupProcess = false
|
||||
}: { showMessage?: boolean; customFileName?: string; autoBackupProcess?: boolean } = {}) {
|
||||
const notificationService = NotificationService.getInstance()
|
||||
if (isManualBackupRunning) {
|
||||
Logger.log('[Backup] Manual backup already in progress')
|
||||
return
|
||||
@ -115,6 +128,16 @@ export async function backupToWebdav({
|
||||
lastSyncError: null
|
||||
})
|
||||
)
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'success',
|
||||
title: i18n.t('common.success'),
|
||||
message: i18n.t('message.backup.success'),
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'backup'
|
||||
})
|
||||
|
||||
if (showMessage && !autoBackupProcess) {
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
}
|
||||
@ -173,7 +196,15 @@ export async function backupToWebdav({
|
||||
if (autoBackupProcess) {
|
||||
throw error
|
||||
}
|
||||
|
||||
notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'error',
|
||||
title: i18n.t('common.error'),
|
||||
message: i18n.t('message.backup.failed'),
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'backup'
|
||||
})
|
||||
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
||||
console.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||
showMessage &&
|
||||
|
||||
55
src/renderer/src/services/NotificationService.ts
Normal file
55
src/renderer/src/services/NotificationService.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Notification } from '@renderer/types/notification'
|
||||
|
||||
import { NotificationQueue } from '../queue/NotificationQueue'
|
||||
|
||||
export class NotificationService {
|
||||
private static instance: NotificationService
|
||||
private queue: NotificationQueue
|
||||
|
||||
private constructor() {
|
||||
this.queue = NotificationQueue.getInstance()
|
||||
this.setupNotificationClickHandler()
|
||||
}
|
||||
|
||||
public static getInstance(): NotificationService {
|
||||
if (!NotificationService.instance) {
|
||||
NotificationService.instance = new NotificationService()
|
||||
}
|
||||
return NotificationService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
* @param notification 要发送的通知
|
||||
*/
|
||||
public async send(notification: Notification): Promise<void> {
|
||||
await this.queue.add(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通知点击事件处理
|
||||
*/
|
||||
private setupNotificationClickHandler(): void {
|
||||
// Register an event listener for notification clicks
|
||||
window.electron.ipcRenderer.on('notification-click', (_event, notification: Notification) => {
|
||||
// 根据通知类型处理点击事件
|
||||
if (notification.type === 'action' && notification.onClick) {
|
||||
notification.onClick()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空通知队列
|
||||
*/
|
||||
public clear(): void {
|
||||
this.queue.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列中等待的通知数量
|
||||
*/
|
||||
public get pendingCount(): number {
|
||||
return this.queue.pending
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 104,
|
||||
version: 105,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -1421,6 +1421,14 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'105': (state: RootState) => {
|
||||
try {
|
||||
state.settings.notification = settingsInitialState.notification
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -160,6 +160,12 @@ export interface SettingsState {
|
||||
summaryText: OpenAISummaryText
|
||||
serviceTier: OpenAIServiceTier
|
||||
}
|
||||
// Notification
|
||||
notification: {
|
||||
assistant: boolean
|
||||
backup: boolean
|
||||
knowledgeEmbed: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
@ -285,6 +291,11 @@ export const initialState: SettingsState = {
|
||||
openAI: {
|
||||
summaryText: 'off',
|
||||
serviceTier: 'auto'
|
||||
},
|
||||
notification: {
|
||||
assistant: true,
|
||||
backup: true,
|
||||
knowledgeEmbed: true
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,6 +624,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setOpenAIServiceTier: (state, action: PayloadAction<OpenAIServiceTier>) => {
|
||||
state.openAI.serviceTier = action.payload
|
||||
},
|
||||
setNotificationSettings: (state, action: PayloadAction<SettingsState['notification']>) => {
|
||||
state.notification = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -709,7 +723,8 @@ export const {
|
||||
setExportMenuOptions,
|
||||
setEnableBackspaceDeleteModel,
|
||||
setOpenAISummaryText,
|
||||
setOpenAIServiceTier
|
||||
setOpenAIServiceTier,
|
||||
setNotificationSettings
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@ -2,6 +2,7 @@ import db from '@renderer/databases'
|
||||
import { autoRenameTopic } from '@renderer/hooks/useTopic'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { NotificationService } from '@renderer/services/NotificationService'
|
||||
import { createStreamProcessor, type StreamProcessorCallbacks } from '@renderer/services/StreamProcessingService'
|
||||
import { estimateMessagesUsage } from '@renderer/services/TokenService'
|
||||
import store from '@renderer/store'
|
||||
@ -18,6 +19,7 @@ import type {
|
||||
} from '@renderer/types/newMessage'
|
||||
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { Response } from '@renderer/types/newMessage'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { isAbortError } from '@renderer/utils/error'
|
||||
import { extractUrlsFromMarkdown } from '@renderer/utils/linkConverter'
|
||||
import {
|
||||
@ -32,9 +34,10 @@ import {
|
||||
createTranslationBlock,
|
||||
resetAssistantMessage
|
||||
} from '@renderer/utils/messageUtils/create'
|
||||
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import { getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
|
||||
import { t } from 'i18next'
|
||||
import { throttle } from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { AppDispatch, RootState } from '../index'
|
||||
import { removeManyBlocks, updateOneBlock, upsertManyBlocks, upsertOneBlock } from '../messageBlock'
|
||||
@ -254,6 +257,7 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
let citationBlockId: string | null = null
|
||||
let mainTextBlockId: string | null = null
|
||||
const toolCallIdToBlockIdMap = new Map<string, string>()
|
||||
const notificationService = NotificationService.getInstance()
|
||||
|
||||
const handleBlockTransition = async (newBlock: MessageBlock, newBlockType: MessageBlockType) => {
|
||||
lastBlockId = newBlock.id
|
||||
@ -585,6 +589,16 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
status: error.status || error.code,
|
||||
requestId: error.request_id
|
||||
}
|
||||
await notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'error',
|
||||
title: t('notification.assistant'),
|
||||
message: serializableError.message,
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'assistant'
|
||||
})
|
||||
|
||||
if (lastBlockId) {
|
||||
// 更改上一个block的状态为ERROR
|
||||
const changes: Partial<MessageBlock> = {
|
||||
@ -631,6 +645,17 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
saveUpdatedBlockToDB(lastBlockId, assistantMsgId, topicId, getState)
|
||||
}
|
||||
|
||||
const content = getMainTextContent(finalAssistantMsg)
|
||||
await notificationService.send({
|
||||
id: uuid(),
|
||||
type: 'success',
|
||||
title: t('notification.assistant'),
|
||||
message: content.length > 50 ? content.slice(0, 47) + '...' : content,
|
||||
silent: false,
|
||||
timestamp: Date.now(),
|
||||
source: 'assistant'
|
||||
})
|
||||
|
||||
// 更新topic的name
|
||||
autoRenameTopic(assistant, topicId)
|
||||
|
||||
@ -1287,7 +1312,7 @@ export const cloneMessagesToNewTopicThunk =
|
||||
|
||||
// 3. Clone Messages and Blocks with New IDs
|
||||
for (const oldMessage of messagesToClone) {
|
||||
const newMsgId = uuidv4()
|
||||
const newMsgId = uuid()
|
||||
originalToNewMsgIdMap.set(oldMessage.id, newMsgId) // Store mapping for all cloned messages
|
||||
|
||||
let newAskId: string | undefined = undefined // Initialize newAskId
|
||||
@ -1313,7 +1338,7 @@ export const cloneMessagesToNewTopicThunk =
|
||||
for (const oldBlockId of oldMessage.blocks) {
|
||||
const oldBlock = state.messageBlocks.entities[oldBlockId]
|
||||
if (oldBlock) {
|
||||
const newBlockId = uuidv4()
|
||||
const newBlockId = uuid()
|
||||
const newBlock: MessageBlock = {
|
||||
...oldBlock,
|
||||
id: newBlockId,
|
||||
|
||||
29
src/renderer/src/types/notification.ts
Normal file
29
src/renderer/src/types/notification.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export type NotificationType = 'progress' | 'success' | 'error' | 'warning' | 'info' | 'action'
|
||||
export type NotificationSource = 'assistant' | 'backup' | 'knowledgeEmbed' | 'update'
|
||||
|
||||
export interface Notification<T = any> {
|
||||
/** 通知唯一标识 */
|
||||
id: string
|
||||
/** 通知分类 */
|
||||
type: NotificationType
|
||||
/** 简要标题,用于列表或弹框的主文案 */
|
||||
title: string
|
||||
/** 详细描述,可包含执行上下文、结果摘要等 */
|
||||
message: string
|
||||
/** 时间戳,便于排序与去重 */
|
||||
timestamp: number
|
||||
/** 可选的进度值(0~1),针对长任务反馈 */
|
||||
progress?: number
|
||||
/** 附加元数据,T 可定制各种业务字段 */
|
||||
meta?: T
|
||||
/** 点击或操作回调标识,前端可根据此字段触发路由或函数 */
|
||||
actionKey?: string
|
||||
/** 声音/声音开关标识,结合用户偏好决定是否播放 */
|
||||
silent?: boolean
|
||||
/** 渠道:系统级(OS 通知)|应用内(UI 通知) */
|
||||
channel?: 'system' | 'in-app'
|
||||
/** 点击回调函数,仅在 type 为 'action' 时有效 */
|
||||
onClick?: () => void
|
||||
/** 通知源 */
|
||||
source: NotificationSource
|
||||
}
|
||||
3
src/renderer/src/utils/window.ts
Normal file
3
src/renderer/src/utils/window.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const isFocused = () => {
|
||||
return document.hasFocus()
|
||||
}
|
||||
69
yarn.lock
69
yarn.lock
@ -5807,6 +5807,7 @@ __metadata:
|
||||
electron-devtools-installer: "npm:^3.2.0"
|
||||
electron-icon-builder: "npm:^2.0.1"
|
||||
electron-log: "npm:^5.1.5"
|
||||
electron-notification-state: "npm:^1.0.4"
|
||||
electron-store: "npm:^8.2.0"
|
||||
electron-updater: "npm:6.6.4"
|
||||
electron-vite: "npm:^3.1.0"
|
||||
@ -6534,6 +6535,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bindings@npm:^1.3.0, bindings@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "bindings@npm:1.5.0"
|
||||
dependencies:
|
||||
file-uri-to-path: "npm:1.0.0"
|
||||
checksum: 10c0/3dab2491b4bb24124252a91e656803eac24292473e56554e35bbfe3cc1875332cfa77600c3bac7564049dc95075bf6fcc63a4609920ff2d64d0fe405fcf0d4ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"birecord@npm:^0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "birecord@npm:0.1.1"
|
||||
@ -8845,6 +8855,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-notification-state@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "electron-notification-state@npm:1.0.4"
|
||||
dependencies:
|
||||
macos-notification-state: "npm:^1.1.0"
|
||||
windows-notification-state: "npm:^1.3.0"
|
||||
windows-quiet-hours: "npm:^1.2.2"
|
||||
checksum: 10c0/040af9b2d6966ce962712e54fe325e5863cf895d41e78dd31caf3da5fa2708c851321ec5c4ce84f192c25c72050d7aaa8d443d2c057712fe02f93f86d8d865b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-publish@npm:26.0.13":
|
||||
version: 26.0.13
|
||||
resolution: "electron-publish@npm:26.0.13"
|
||||
@ -10169,6 +10190,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-uri-to-path@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "file-uri-to-path@npm:1.0.0"
|
||||
checksum: 10c0/3b545e3a341d322d368e880e1c204ef55f1d45cdea65f7efc6c6ce9e0c4d22d802d5629320eb779d006fe59624ac17b0e848d83cc5af7cd101f206cb704f5519
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-url@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "file-url@npm:2.0.2"
|
||||
@ -12919,6 +12947,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"macos-notification-state@npm:^1.1.0":
|
||||
version: 1.3.6
|
||||
resolution: "macos-notification-state@npm:1.3.6"
|
||||
dependencies:
|
||||
bindings: "npm:^1.5.0"
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/2b9c2d358e7e0d331506ef97a808c6e16b26de32c4240ba2c0aee2e826007e8aa393f3389883975aa68734a4bb38466662f084523710ce4688b728515161d7f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.30.17":
|
||||
version: 0.30.17
|
||||
resolution: "magic-string@npm:0.30.17"
|
||||
@ -14388,6 +14426,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nan@npm:^2.14.0, nan@npm:^2.7.0":
|
||||
version: 2.22.2
|
||||
resolution: "nan@npm:2.22.2"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/971f963b8120631880fa47a389c71b00cadc1c1b00ef8f147782a3f4387d4fc8195d0695911272d57438c11562fb27b24c4ae5f8c05d5e4eeb4478ba51bb73c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^3.3.7, nanoid@npm:^3.3.8":
|
||||
version: 3.3.11
|
||||
resolution: "nanoid@npm:3.3.11"
|
||||
@ -19793,6 +19840,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"windows-notification-state@npm:^1.3.0":
|
||||
version: 1.3.4
|
||||
resolution: "windows-notification-state@npm:1.3.4"
|
||||
dependencies:
|
||||
bindings: "npm:^1.5.0"
|
||||
nan: "npm:^2.14.0"
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/d92e59acb21369204b5ff3d4633b216fe6c374f27eed5a62ad25b859b4201023a2903b64d1d11e8ded266da6109b6a68fb95753c75e873cc173dd80e91e75c5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"windows-quiet-hours@npm:^1.2.2":
|
||||
version: 1.2.7
|
||||
resolution: "windows-quiet-hours@npm:1.2.7"
|
||||
dependencies:
|
||||
bindings: "npm:^1.3.0"
|
||||
nan: "npm:^2.7.0"
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/1a1076f40f0a66ebe6cc39e7f52a199ae59f6e94e733e363a6e1e168c8b38435020404f66f35268174a11015104e8ee8265f2e190a664d7923a70822d327a887
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"windows-system-proxy@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "windows-system-proxy@npm:1.0.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user