diff --git a/electron/services/config.ts b/electron/services/config.ts index 2973e2d..37d0be9 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -58,6 +58,7 @@ interface ConfigSchema { // 通知 notificationEnabled: boolean + aiInsightNotificationEnabled: boolean notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' notificationFilterMode: 'all' | 'whitelist' | 'blacklist' notificationFilterList: string[] @@ -188,6 +189,7 @@ export class ConfigService { ignoredUpdateVersion: '', updateChannel: 'auto', notificationEnabled: true, + aiInsightNotificationEnabled: true, notificationPosition: 'top-right', notificationFilterMode: 'all', notificationFilterList: [], diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 5554a29..d349a17 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -18,11 +18,12 @@ import http from 'http' import fs from 'fs' import path from 'path' import { URL } from 'url' -import { app, Notification } from 'electron' +import { app } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' import { snsService } from './snsService' import { weiboService } from './social/weiboService' +import { showNotification } from '../windows/notificationWindow' // ─── 常量 ──────────────────────────────────────────────────────────────────── @@ -41,6 +42,7 @@ const API_MAX_TOKENS_DEFAULT = 200 const API_MAX_TOKENS_MIN = 1 const API_MAX_TOKENS_MAX = 65_535 const API_TEMPERATURE = 0.7 +const INSIGHT_NOTIFICATION_AVATAR_URL = './assets/insight/AI_Insight.png' /** 沉默天数阈值默认值 */ const DEFAULT_SILENCE_DAYS = 3 @@ -518,7 +520,13 @@ class InsightService { displayName, triggerReason: 'activity' }) - return { success: true, message: `已向「${displayName}」发送测试见解,请查看右下角弹窗` } + const notificationEnabled = this.config.get('aiInsightNotificationEnabled') !== false + return { + success: true, + message: notificationEnabled + ? `已向「${displayName}」发送测试见解,请查看通知弹窗` + : `已生成「${displayName}」的测试见解,AI 见解消息通知当前已关闭` + } } catch (e) { return { success: false, message: `测试失败:${(e as Error).message}` } } @@ -1250,14 +1258,20 @@ ${topMentionText} const insight = result.slice(0, 120) const notifTitle = `见解 · ${resolvedDisplayName}` - insightLog('INFO', `推送通知 → ${resolvedDisplayName}: ${insight}`) + const insightNotificationEnabled = this.config.get('aiInsightNotificationEnabled') !== false + if (insightNotificationEnabled) { + insightLog('INFO', `推送通知 → ${resolvedDisplayName}: ${insight}`) - // 渠道一:Electron 原生系统通知 - if (Notification.isSupported()) { - const notif = new Notification({ title: notifTitle, body: insight, silent: false }) - notif.show() + // 渠道一:应用内通知窗口。AI 见解使用独立通知开关,不受新消息通知开关和会话过滤影响。 + await showNotification({ + title: notifTitle, + content: insight, + avatarUrl: INSIGHT_NOTIFICATION_AVATAR_URL, + sessionId, + channel: 'ai-insight' + }) } else { - insightLog('WARN', '当前系统不支持原生通知') + insightLog('INFO', `AI 见解消息通知已关闭,跳过应用通知 → ${resolvedDisplayName}: ${insight}`) } // 渠道二:Telegram Bot 推送(可选) @@ -1278,7 +1292,7 @@ ${topMentionText} } } - insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`) + insightLog('INFO', `已完成 ${resolvedDisplayName} 的见解处理`) this.recordTrigger(sessionId) } catch (e) { insightDebugSection( diff --git a/electron/windows/notificationWindow.ts b/electron/windows/notificationWindow.ts index 21bbf01..8e06fec 100644 --- a/electron/windows/notificationWindow.ts +++ b/electron/windows/notificationWindow.ts @@ -109,25 +109,33 @@ export function createNotificationWindow() { export async function showNotification(data: any) { // 先检查配置 const config = ConfigService.getInstance(); - const enabled = await config.get("notificationEnabled"); - if (enabled === false) return; // 默认为 true - - // 检查会话过滤 - const filterMode = config.get("notificationFilterMode") || "all"; - const filterList = config.get("notificationFilterList") || []; const sessionId = typeof data.sessionId === "string" ? data.sessionId : ""; - // 系统通知(如 "WeFlow 准备就绪")不是聊天消息,不应受会话白/黑名单影响 - const isSystemNotification = sessionId.startsWith("weflow-"); + const channel = typeof data.channel === "string" ? data.channel : ""; + const isAiInsightNotification = channel === "ai-insight"; - if (!isSystemNotification && filterMode !== "all") { - const isInList = sessionId !== "" && filterList.includes(sessionId); - if (filterMode === "whitelist" && !isInList) { - // 白名单模式:不在列表中则不显示(空列表视为全部拦截) - return; - } - if (filterMode === "blacklist" && isInList) { - // 黑名单模式:在列表中则不显示 - return; + if (isAiInsightNotification) { + const enabled = await config.get("aiInsightNotificationEnabled"); + if (enabled === false) return; // 默认为 true + } else { + const enabled = await config.get("notificationEnabled"); + if (enabled === false) return; // 默认为 true + + // 检查会话过滤 + const filterMode = config.get("notificationFilterMode") || "all"; + const filterList = config.get("notificationFilterList") || []; + // 系统通知(如 "WeFlow 准备就绪")不是聊天消息,不应受会话白/黑名单影响 + const isSystemNotification = sessionId.startsWith("weflow-"); + + if (!isSystemNotification && filterMode !== "all") { + const isInList = sessionId !== "" && filterList.includes(sessionId); + if (filterMode === "whitelist" && !isInList) { + // 白名单模式:不在列表中则不显示(空列表视为全部拦截) + return; + } + if (filterMode === "blacklist" && isInList) { + // 黑名单模式:在列表中则不显示 + return; + } } } diff --git a/public/assets/insight/AI_Insight.png b/public/assets/insight/AI_Insight.png new file mode 100644 index 0000000..1e0d148 Binary files /dev/null and b/public/assets/insight/AI_Insight.png differ diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index fb53979..8d50968 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -199,6 +199,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) const [notificationEnabled, setNotificationEnabled] = useState(true) + const [aiInsightNotificationEnabled, setAiInsightNotificationEnabled] = useState(true) const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right') const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all') const [notificationFilterList, setNotificationFilterList] = useState([]) @@ -458,6 +459,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAutoTranscribe = await configService.getAutoTranscribeVoice() const savedTranscribeLanguages = await configService.getTranscribeLanguages() const savedNotificationEnabled = await configService.getNotificationEnabled() + const savedAiInsightNotificationEnabled = await configService.getAiInsightNotificationEnabled() const savedNotificationPosition = await configService.getNotificationPosition() const savedNotificationFilterMode = await configService.getNotificationFilterMode() const savedNotificationFilterList = await configService.getNotificationFilterList() @@ -512,6 +514,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setTranscribeLanguages(savedTranscribeLanguages) setNotificationEnabled(savedNotificationEnabled) + setAiInsightNotificationEnabled(savedAiInsightNotificationEnabled) setNotificationPosition(savedNotificationPosition) setNotificationFilterMode(savedNotificationFilterMode) setNotificationFilterList(savedNotificationFilterList) @@ -1903,6 +1906,29 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { +
+ + 仅控制 AI 见解弹窗,不影响新消息通知、会话过滤或 Telegram 推送 +
+ {aiInsightNotificationEnabled ? '已开启' : '已关闭'} + +
+
+
选择通知弹窗在屏幕上的显示位置 @@ -3209,7 +3235,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- 开启后,AI 会在后台默默分析聊天数据,在合适的时机通过右下角弹窗送出一针见血的见解——例如提醒你久未联系的朋友,或对你刚刚的对话提出回复建议。默认关闭,所有分析均在本地发起请求,不经过任何第三方中间服务。 + 开启后,AI 会在后台默默分析聊天数据,在合适的时机通过应用通知送出一针见血的见解——例如提醒你久未联系的朋友,或对你刚刚的对话提出回复建议。默认关闭,所有分析均在本地发起请求,不经过任何第三方中间服务。
{aiInsightEnabled ? '已开启' : '已关闭'} diff --git a/src/services/config.ts b/src/services/config.ts index 0d6588e..777e4dd 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -66,6 +66,7 @@ export const CONFIG_KEYS = { // 通知 NOTIFICATION_ENABLED: 'notificationEnabled', + AI_INSIGHT_NOTIFICATION_ENABLED: 'aiInsightNotificationEnabled', NOTIFICATION_POSITION: 'notificationPosition', NOTIFICATION_FILTER_MODE: 'notificationFilterMode', NOTIFICATION_FILTER_LIST: 'notificationFilterList', @@ -1677,6 +1678,15 @@ export async function setNotificationEnabled(enabled: boolean): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_ENABLED, enabled) } +export async function getAiInsightNotificationEnabled(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_NOTIFICATION_ENABLED) + return value !== false +} + +export async function setAiInsightNotificationEnabled(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_NOTIFICATION_ENABLED, enabled) +} + // 获取通知位置 export async function getNotificationPosition(): Promise<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'> { const value = await config.get(CONFIG_KEYS.NOTIFICATION_POSITION)