fix: add channel property to notifications for backup and assistant messages (#8120)

* fix: add channel property to notifications for backup and assistant messages

* Add notification tip and improve assistant notification logic

Added a tooltip in the notification settings UI to clarify that only messages exceeding 30 seconds will trigger a reminder. Updated i18n files for all supported languages with the new tip. Modified notification logic to only send notifications for assistant responses or errors if the message duration exceeds 30 seconds and the user is not on the home page or the window is not focused.

* Remove duplicate InfoCircleOutlined import

Consolidated the import of InfoCircleOutlined from '@ant-design/icons' to avoid redundancy in GeneralSettings.tsx.

* fix: add isFocused mock and simplify createMockStore

Added a mock for isFocused in the window utility and refactored createMockStore to return the configured store directly. This improves test setup clarity and ensures all necessary window utilities are mocked.
This commit is contained in:
自由的世界人 2025-07-15 19:25:55 +08:00 committed by GitHub
parent 397965f6e9
commit a6db53873a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 47 additions and 31 deletions

View File

@ -1,8 +1,6 @@
import { BrowserWindow, Notification as ElectronNotification } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
import icon from '../../../build/icon.png?asset'
class NotificationService {
private window: BrowserWindow
@ -15,8 +13,7 @@ class NotificationService {
// 使用 Electron Notification API
const electronNotification = new ElectronNotification({
title: notification.title,
body: notification.message,
icon: icon
body: notification.message
})
electronNotification.on('click', () => {

View File

@ -31,7 +31,8 @@ export default function useUpdateHandler() {
title: t('button.update_available'),
message: t('button.update_available', { version: releaseInfo.version }),
timestamp: Date.now(),
source: 'update'
source: 'update',
channel: 'system'
})
dispatch(
setUpdateState({

View File

@ -906,7 +906,8 @@
"notification": {
"assistant": "Assistant Response",
"knowledge.error": "{{error}}",
"knowledge.success": "Successfully added {{type}} to the knowledge base"
"knowledge.success": "Successfully added {{type}} to the knowledge base",
"tip": "If the response is successful, then only messages exceeding 30 seconds will trigger a reminder"
},
"ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",

View File

@ -906,7 +906,8 @@
"notification": {
"assistant": "助手回應",
"knowledge.error": "{{error}}",
"knowledge.success": "ナレッジベースに{{type}}を正常に追加しました"
"knowledge.success": "ナレッジベースに{{type}}を正常に追加しました",
"tip": "応答が成功した場合、30秒を超えるメッセージのみに通知を行います"
},
"ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",

View File

@ -906,7 +906,8 @@
"notification": {
"assistant": "Ответ ассистента",
"knowledge.error": "{{error}}",
"knowledge.success": "Успешно добавлено {{type}} в базу знаний"
"knowledge.success": "Успешно добавлено {{type}} в базу знаний",
"tip": "Если ответ успешен, уведомление выдается только по сообщениям, превышающим 30 секунд"
},
"ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",

View File

@ -906,7 +906,8 @@
"notification": {
"assistant": "助手响应",
"knowledge.error": "{{error}}",
"knowledge.success": "成功添加 {{type}} 到知识库"
"knowledge.success": "成功添加 {{type}} 到知识库",
"tip": "如果响应成功则只针对超过30秒的消息进行提醒"
},
"ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5 分钟)",

View File

@ -906,7 +906,8 @@
"notification": {
"assistant": "助手回應",
"knowledge.error": "無法將 {{type}} 加入知識庫: {{error}}",
"knowledge.success": "成功將 {{type}} 新增至知識庫"
"knowledge.success": "成功將 {{type}} 新增至知識庫",
"tip": "如果回應成功則只針對超過30秒的訊息發出提醒"
},
"ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",

View File

@ -1,3 +1,4 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
@ -16,7 +17,7 @@ import { LanguageVarious } from '@renderer/types'
import { NotificationSource } from '@renderer/types/notification'
import { isValidProxyUrl } from '@renderer/utils'
import { defaultLanguage } from '@shared/config/constant'
import { Flex, Input, Switch } from 'antd'
import { Flex, Input, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -261,7 +262,12 @@ const GeneralSettings: FC = () => {
<SettingTitle>{t('settings.notification.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.notification.assistant')}</SettingRowTitle>
<SettingRowTitle style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span>{t('settings.notification.assistant')}</span>
<Tooltip title={t('notification.tip')} placement="right">
<InfoCircleOutlined style={{ cursor: 'pointer' }} />
</Tooltip>
</SettingRowTitle>
<Switch checked={notificationSettings.assistant} onChange={(v) => handleNotificationChange('assistant', v)} />
</SettingRow>
<SettingDivider />

View File

@ -94,7 +94,8 @@ export async function restore() {
message: i18n.t('message.restore.success'),
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
} catch (error) {
Logger.error('[Backup] restore: Error restoring backup file:', error)
@ -129,6 +130,8 @@ export async function reset() {
// 备份到 webdav
/**
* @param showMessage
* @param customFileName
* @param autoBackupProcess
* if call in auto backup process, not show any message, any error will be thrown
*/
@ -189,7 +192,8 @@ export async function backupToWebdav({
message: i18n.t('message.backup.success'),
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
@ -258,7 +262,8 @@ export async function backupToWebdav({
message: error.message,
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
@ -354,7 +359,8 @@ export async function backupToS3({
message: i18n.t('message.backup.success'),
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
@ -407,7 +413,8 @@ export async function backupToS3({
message: error.message,
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
store.dispatch(setS3SyncState({ lastSyncError: error.message }))
console.error('[Backup] backupToS3: Error uploading file to S3:', error)
@ -935,7 +942,8 @@ export async function backupToLocal({
message: i18n.t('message.backup.success'),
silent: false,
timestamp: Date.now(),
source: 'backup'
source: 'backup',
channel: 'system'
})
}

View File

@ -142,7 +142,8 @@ vi.mock('@renderer/services/EventService', () => ({
}))
vi.mock('@renderer/utils/window', () => ({
isOnHomePage: vi.fn(() => true)
isOnHomePage: vi.fn(() => true),
isFocused: vi.fn(() => true)
}))
vi.mock('@renderer/hooks/useTopic', () => ({
@ -231,11 +232,10 @@ const reducer = combineReducers({
})
const createMockStore = () => {
const store = configureStore({
return configureStore({
reducer: reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false })
})
return store
}
// Helper function to simulate processing chunks through the stream processor

View File

@ -42,9 +42,8 @@ import {
resetAssistantMessage
} from '@renderer/utils/messageUtils/create'
import { findMainTextBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import { getTopicQueue } from '@renderer/utils/queue'
import { waitForTopicQueue } from '@renderer/utils/queue'
import { isOnHomePage } from '@renderer/utils/window'
import { getTopicQueue, waitForTopicQueue } from '@renderer/utils/queue'
import { isFocused, isOnHomePage } from '@renderer/utils/window'
import { t } from 'i18next'
import { isEmpty, throttle } from 'lodash'
import { LRUCache } from 'lru-cache'
@ -719,7 +718,8 @@ export const streamCallback = (
status: error.status || error.code,
requestId: error.request_id
}
if (!isOnHomePage()) {
const msgDuration = Date.now() - startTime
if ((!isOnHomePage() && msgDuration > 30 * 1000) || (!isFocused() && msgDuration > 30 * 1000)) {
await notificationService.send({
id: uuid(),
type: 'error',
@ -789,10 +789,9 @@ export const streamCallback = (
smartBlockUpdate(possibleBlockId, changes, lastBlockType!, true)
}
const endTime = Date.now()
const duration = endTime - startTime
const content = getMainTextContent(finalAssistantMsg)
if (!isOnHomePage() && duration > 60 * 1000) {
const msgDuration = Date.now() - startTime
if ((!isOnHomePage() && msgDuration > 30 * 1000) || (!isFocused() && msgDuration > 30 * 1000)) {
await notificationService.send({
id: uuid(),
type: 'success',
@ -800,7 +799,8 @@ export const streamCallback = (
message: content.length > 50 ? content.slice(0, 47) + '...' : content,
silent: false,
timestamp: Date.now(),
source: 'assistant'
source: 'assistant',
channel: 'system'
})
}
@ -813,8 +813,7 @@ export const streamCallback = (
response?.usage?.prompt_tokens === 0 ||
response?.usage?.completion_tokens === 0)
) {
const usage = await estimateMessagesUsage({ assistant, messages: finalContextWithAssistant })
response.usage = usage
response.usage = await estimateMessagesUsage({ assistant, messages: finalContextWithAssistant })
}
// dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}