mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-06 23:15:58 +00:00
Merge pull request #655 from FATFATHAO/feat/linux-notification
[#654] fix: 更改linux中的消息通知走D-bus总线
This commit is contained in:
@@ -27,7 +27,7 @@ import { windowsHelloService } from './services/windowsHelloService'
|
|||||||
import { exportCardDiagnosticsService } from './services/exportCardDiagnosticsService'
|
import { exportCardDiagnosticsService } from './services/exportCardDiagnosticsService'
|
||||||
import { cloudControlService } from './services/cloudControlService'
|
import { cloudControlService } from './services/cloudControlService'
|
||||||
|
|
||||||
import { destroyNotificationWindow, registerNotificationHandlers, showNotification } from './windows/notificationWindow'
|
import { destroyNotificationWindow, registerNotificationHandlers, showNotification, setNotificationNavigateHandler } from './windows/notificationWindow'
|
||||||
import { httpService } from './services/httpService'
|
import { httpService } from './services/httpService'
|
||||||
import { messagePushService } from './services/messagePushService'
|
import { messagePushService } from './services/messagePushService'
|
||||||
import { bizService } from './services/bizService'
|
import { bizService } from './services/bizService'
|
||||||
@@ -740,6 +740,14 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
|||||||
win.webContents.send('navigate-to-session', sessionId)
|
win.webContents.send('navigate-to-session', sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 设置用于D-Bus通知的Linux通知导航处理程序
|
||||||
|
setNotificationNavigateHandler((sessionId: string) => {
|
||||||
|
if (win.isMinimized()) win.restore()
|
||||||
|
win.show()
|
||||||
|
win.focus()
|
||||||
|
win.webContents.send('navigate-to-session', sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
// 拦截请求,修改 Referer 和 User-Agent 以通过微信 CDN 鉴权
|
// 拦截请求,修改 Referer 和 User-Agent 以通过微信 CDN 鉴权
|
||||||
session.defaultSession.webRequest.onBeforeSendHeaders(
|
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||||||
{
|
{
|
||||||
|
|||||||
344
electron/services/linuxNotificationService.ts
Normal file
344
electron/services/linuxNotificationService.ts
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import dbus from "dbus-native";
|
||||||
|
import https from "https";
|
||||||
|
import http, { IncomingMessage } from "http";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
const BUS_NAME = "org.freedesktop.Notifications";
|
||||||
|
const OBJECT_PATH = "/org/freedesktop/Notifications";
|
||||||
|
|
||||||
|
export interface LinuxNotificationData {
|
||||||
|
sessionId?: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
expireTimeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationCallback = (sessionId: string) => void;
|
||||||
|
|
||||||
|
let sessionBus: dbus.DBusConnection | null = null;
|
||||||
|
let notificationCallbacks: NotificationCallback[] = [];
|
||||||
|
let pendingNotifications: Map<number, LinuxNotificationData> = new Map();
|
||||||
|
|
||||||
|
// 头像缓存:url->localFilePath
|
||||||
|
const avatarCache: Map<string, string> = new Map();
|
||||||
|
// 缓存目录
|
||||||
|
let avatarCacheDir: string | null = null;
|
||||||
|
|
||||||
|
async function getSessionBus(): Promise<dbus.DBusConnection> {
|
||||||
|
if (!sessionBus) {
|
||||||
|
sessionBus = dbus.sessionBus();
|
||||||
|
|
||||||
|
// 挂载底层socket的error事件,防止掉线即可
|
||||||
|
sessionBus.connection.on("error", (err: Error) => {
|
||||||
|
console.error("[LinuxNotification] D-Bus connection error:", err);
|
||||||
|
sessionBus = null; // 报错清理死对象
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sessionBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保缓存目录存在
|
||||||
|
async function ensureCacheDir(): Promise<string> {
|
||||||
|
if (!avatarCacheDir) {
|
||||||
|
avatarCacheDir = join(app.getPath("temp"), "weflow-avatars");
|
||||||
|
try {
|
||||||
|
await fs.mkdir(avatarCacheDir, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[LinuxNotification] Failed to create avatar cache dir:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avatarCacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载头像到本地临时文件
|
||||||
|
async function downloadAvatarToLocal(url: string): Promise<string | null> {
|
||||||
|
// 检查缓存
|
||||||
|
if (avatarCache.has(url)) {
|
||||||
|
return avatarCache.get(url) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheDir = await ensureCacheDir();
|
||||||
|
// 生成唯一文件名
|
||||||
|
const fileName = `avatar_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.png`;
|
||||||
|
const localPath = join(cacheDir, fileName);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
// 微信 CDN 需要特殊的请求头才能下载图片
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351",
|
||||||
|
Referer: "https://servicewechat.com/",
|
||||||
|
Accept:
|
||||||
|
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const callback = (res: IncomingMessage) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
await fs.writeFile(localPath, buffer);
|
||||||
|
avatarCache.set(url, localPath);
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.on("error", reject);
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = url.startsWith("https")
|
||||||
|
? https.get(url, options, callback)
|
||||||
|
: http.get(url, options, callback);
|
||||||
|
|
||||||
|
req.on("error", reject);
|
||||||
|
req.setTimeout(10000, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("Download timeout"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[LinuxNotification] Avatar downloaded: ${url} -> ${localPath}`,
|
||||||
|
);
|
||||||
|
return localPath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Failed to download avatar:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showLinuxNotification(
|
||||||
|
data: LinuxNotificationData,
|
||||||
|
): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const bus = await getSessionBus();
|
||||||
|
|
||||||
|
const appName = "WeFlow";
|
||||||
|
const replaceId = 0;
|
||||||
|
const expireTimeout = data.expireTimeout ?? 5000;
|
||||||
|
|
||||||
|
// 处理头像:下载到本地或使用URL
|
||||||
|
let appIcon = "";
|
||||||
|
let hints: any[] = [];
|
||||||
|
if (data.avatarUrl) {
|
||||||
|
// 优先尝试下载到本地
|
||||||
|
const localPath = await downloadAvatarToLocal(data.avatarUrl);
|
||||||
|
if (localPath) {
|
||||||
|
hints = [["image-path", ["s", localPath]]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
bus.invoke(
|
||||||
|
{
|
||||||
|
destination: BUS_NAME,
|
||||||
|
path: OBJECT_PATH,
|
||||||
|
interface: "org.freedesktop.Notifications",
|
||||||
|
member: "Notify",
|
||||||
|
signature: "susssasa{sv}i",
|
||||||
|
body: [
|
||||||
|
appName,
|
||||||
|
replaceId,
|
||||||
|
appIcon,
|
||||||
|
data.title,
|
||||||
|
data.content,
|
||||||
|
["default", "打开"], // 提供default action,否则系统不会抛出点击事件
|
||||||
|
hints,
|
||||||
|
// [], // 传空数组以避开a{sv}变体的序列化崩溃,有pendingNotifications映射维护保证不出错
|
||||||
|
expireTimeout,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(err: Error | null, result: any) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[LinuxNotification] Notify error:", err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notificationId =
|
||||||
|
typeof result === "number" ? result : result[0];
|
||||||
|
if (data.sessionId) {
|
||||||
|
// 依赖Map实现点击追踪,没有使用D-Bus hints
|
||||||
|
pendingNotifications.set(notificationId, data);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[LinuxNotification] Shown notification ${notificationId}: ${data.title}, icon: ${appIcon || "none"}`,
|
||||||
|
);
|
||||||
|
resolve(notificationId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Failed to show notification:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeLinuxNotification(
|
||||||
|
notificationId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const bus = await getSessionBus();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
bus.invoke(
|
||||||
|
{
|
||||||
|
destination: BUS_NAME,
|
||||||
|
path: OBJECT_PATH,
|
||||||
|
interface: "org.freedesktop.Notifications",
|
||||||
|
member: "CloseNotification",
|
||||||
|
signature: "u",
|
||||||
|
body: [notificationId],
|
||||||
|
},
|
||||||
|
(err: Error | null) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[LinuxNotification] CloseNotification error:", err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingNotifications.delete(notificationId);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Failed to close notification:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCapabilities(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const bus = await getSessionBus();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
bus.invoke(
|
||||||
|
{
|
||||||
|
destination: BUS_NAME,
|
||||||
|
path: OBJECT_PATH,
|
||||||
|
interface: "org.freedesktop.Notifications",
|
||||||
|
member: "GetCapabilities",
|
||||||
|
},
|
||||||
|
(err: Error | null, result: any) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[LinuxNotification] GetCapabilities error:", err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(result as string[]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Failed to get capabilities:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onNotificationAction(callback: NotificationCallback): void {
|
||||||
|
notificationCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeNotificationCallback(
|
||||||
|
callback: NotificationCallback,
|
||||||
|
): void {
|
||||||
|
const index = notificationCallbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
notificationCallbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerNotificationCallback(sessionId: string): void {
|
||||||
|
for (const callback of notificationCallbacks) {
|
||||||
|
try {
|
||||||
|
callback(sessionId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Callback error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initLinuxNotificationService(): Promise<void> {
|
||||||
|
if (process.platform !== "linux") {
|
||||||
|
console.log("[LinuxNotification] Not on Linux, skipping init");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bus = await getSessionBus();
|
||||||
|
|
||||||
|
// 监听底层connection的message事件
|
||||||
|
bus.connection.on("message", (msg: any) => {
|
||||||
|
// type 4表示SIGNAL
|
||||||
|
if (
|
||||||
|
msg.type === 4 &&
|
||||||
|
msg.path === OBJECT_PATH &&
|
||||||
|
msg.interface === "org.freedesktop.Notifications"
|
||||||
|
) {
|
||||||
|
if (msg.member === "ActionInvoked") {
|
||||||
|
const [notificationId, actionId] = msg.body;
|
||||||
|
console.log(
|
||||||
|
`[LinuxNotification] Action invoked: ${notificationId}, ${actionId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果用户点击了通知本体,actionId会是'default'
|
||||||
|
if (actionId === "default") {
|
||||||
|
const data = pendingNotifications.get(notificationId);
|
||||||
|
if (data?.sessionId) {
|
||||||
|
triggerNotificationCallback(data.sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.member === "NotificationClosed") {
|
||||||
|
const [notificationId] = msg.body;
|
||||||
|
pendingNotifications.delete(notificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// AddMatch用来接收信号
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
bus.invoke(
|
||||||
|
{
|
||||||
|
destination: "org.freedesktop.DBus",
|
||||||
|
path: "/org/freedesktop/DBus",
|
||||||
|
interface: "org.freedesktop.DBus",
|
||||||
|
member: "AddMatch",
|
||||||
|
signature: "s",
|
||||||
|
body: ["type='signal',interface='org.freedesktop.Notifications'"],
|
||||||
|
},
|
||||||
|
(err: Error | null) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[LinuxNotification] AddMatch error:", err);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[LinuxNotification] Service initialized");
|
||||||
|
|
||||||
|
// 打印相关日志
|
||||||
|
const caps = await getCapabilities();
|
||||||
|
console.log("[LinuxNotification] Server capabilities:", caps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LinuxNotification] Failed to initialize:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
electron/types/dbus.d.ts
vendored
Normal file
18
electron/types/dbus.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
declare module 'dbus-native' {
|
||||||
|
namespace dbus {
|
||||||
|
interface DBusConnection {
|
||||||
|
invoke(options: any, callback: (err: Error | null, result?: any) => void): void;
|
||||||
|
on(event: string, listener: Function): void;
|
||||||
|
// 底层connection,用于监听signal
|
||||||
|
connection: {
|
||||||
|
on(event: string, listener: Function): void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 声明sessionBus方法
|
||||||
|
function sessionBus(): DBusConnection;
|
||||||
|
function systemBus(): DBusConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = dbus;
|
||||||
|
}
|
||||||
@@ -1,51 +1,66 @@
|
|||||||
import { BrowserWindow, ipcMain, screen } from 'electron'
|
import { BrowserWindow, ipcMain, screen } from "electron";
|
||||||
import { join } from 'path'
|
import { join } from "path";
|
||||||
import { ConfigService } from '../services/config'
|
import { ConfigService } from "../services/config";
|
||||||
|
|
||||||
let notificationWindow: BrowserWindow | null = null
|
// Linux D-Bus通知服务
|
||||||
let closeTimer: NodeJS.Timeout | null = null
|
const isLinux = process.platform === "linux";
|
||||||
|
let linuxNotificationService:
|
||||||
|
| typeof import("../services/linuxNotificationService")
|
||||||
|
| null = null;
|
||||||
|
|
||||||
|
// 用于处理通知点击的回调函数(在Linux上用于导航到会话)
|
||||||
|
let onNotificationNavigate: ((sessionId: string) => void) | null = null;
|
||||||
|
|
||||||
|
export function setNotificationNavigateHandler(
|
||||||
|
callback: (sessionId: string) => void,
|
||||||
|
) {
|
||||||
|
onNotificationNavigate = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notificationWindow: BrowserWindow | null = null;
|
||||||
|
let closeTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
export function destroyNotificationWindow() {
|
export function destroyNotificationWindow() {
|
||||||
if (closeTimer) {
|
if (closeTimer) {
|
||||||
clearTimeout(closeTimer)
|
clearTimeout(closeTimer);
|
||||||
closeTimer = null
|
closeTimer = null;
|
||||||
}
|
}
|
||||||
lastNotificationData = null
|
lastNotificationData = null;
|
||||||
|
|
||||||
if (!notificationWindow || notificationWindow.isDestroyed()) {
|
if (!notificationWindow || notificationWindow.isDestroyed()) {
|
||||||
notificationWindow = null
|
notificationWindow = null;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const win = notificationWindow
|
const win = notificationWindow;
|
||||||
notificationWindow = null
|
notificationWindow = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
win.destroy()
|
win.destroy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[NotificationWindow] Failed to destroy window:', error)
|
console.warn("[NotificationWindow] Failed to destroy window:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNotificationWindow() {
|
export function createNotificationWindow() {
|
||||||
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
||||||
return notificationWindow
|
return notificationWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
const isDev = !!process.env.VITE_DEV_SERVER_URL;
|
||||||
const iconPath = isDev
|
const iconPath = isDev
|
||||||
? join(__dirname, '../../public/icon.ico')
|
? join(__dirname, "../../public/icon.ico")
|
||||||
: join(process.resourcesPath, 'icon.ico')
|
: join(process.resourcesPath, "icon.ico");
|
||||||
|
|
||||||
console.log('[NotificationWindow] Creating window...')
|
console.log("[NotificationWindow] Creating window...");
|
||||||
const width = 344
|
const width = 344;
|
||||||
const height = 114
|
const height = 114;
|
||||||
|
|
||||||
// Update default creation size
|
// Update default creation size
|
||||||
notificationWindow = new BrowserWindow({
|
notificationWindow = new BrowserWindow({
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
type: 'toolbar', // 有助于在某些操作系统上保持置顶
|
type: "toolbar", // 有助于在某些操作系统上保持置顶
|
||||||
frame: false,
|
frame: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
@@ -55,15 +70,15 @@ export function createNotificationWindow() {
|
|||||||
focusable: false, // 不抢占焦点
|
focusable: false, // 不抢占焦点
|
||||||
icon: iconPath,
|
icon: iconPath,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, 'preload.js'), // FIX: Use correct relative path (same dir in dist)
|
preload: join(__dirname, "preload.js"), // FIX: Use correct relative path (same dir in dist)
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
// devTools: true // Enable DevTools
|
// devTools: true // Enable DevTools
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// notificationWindow.webContents.openDevTools({ mode: 'detach' }) // DEBUG: Force Open DevTools
|
// notificationWindow.webContents.openDevTools({ mode: 'detach' }) // DEBUG: Force Open DevTools
|
||||||
notificationWindow.setIgnoreMouseEvents(true, { forward: true }) // 初始点击穿透
|
notificationWindow.setIgnoreMouseEvents(true, { forward: true }); // 初始点击穿透
|
||||||
|
|
||||||
// 处理鼠标事件 (如果需要从渲染进程转发,但目前特定区域处理?)
|
// 处理鼠标事件 (如果需要从渲染进程转发,但目前特定区域处理?)
|
||||||
// 实际上,我们希望窗口可点击。
|
// 实际上,我们希望窗口可点击。
|
||||||
@@ -71,134 +86,228 @@ export function createNotificationWindow() {
|
|||||||
|
|
||||||
const loadUrl = isDev
|
const loadUrl = isDev
|
||||||
? `${process.env.VITE_DEV_SERVER_URL}#/notification-window`
|
? `${process.env.VITE_DEV_SERVER_URL}#/notification-window`
|
||||||
: `file://${join(__dirname, '../dist/index.html')}#/notification-window`
|
: `file://${join(__dirname, "../dist/index.html")}#/notification-window`;
|
||||||
|
|
||||||
console.log('[NotificationWindow] Loading URL:', loadUrl)
|
console.log("[NotificationWindow] Loading URL:", loadUrl);
|
||||||
notificationWindow.loadURL(loadUrl)
|
notificationWindow.loadURL(loadUrl);
|
||||||
|
|
||||||
notificationWindow.on('closed', () => {
|
notificationWindow.on("closed", () => {
|
||||||
notificationWindow = null
|
notificationWindow = null;
|
||||||
})
|
});
|
||||||
|
|
||||||
return notificationWindow
|
return notificationWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showNotification(data: any) {
|
export async function showNotification(data: any) {
|
||||||
// 先检查配置
|
// 先检查配置
|
||||||
const config = ConfigService.getInstance()
|
const config = ConfigService.getInstance();
|
||||||
const enabled = await config.get('notificationEnabled')
|
const enabled = await config.get("notificationEnabled");
|
||||||
if (enabled === false) return // 默认为 true
|
if (enabled === false) return; // 默认为 true
|
||||||
|
|
||||||
// 检查会话过滤
|
// 检查会话过滤
|
||||||
const filterMode = config.get('notificationFilterMode') || 'all'
|
const filterMode = config.get("notificationFilterMode") || "all";
|
||||||
const filterList = config.get('notificationFilterList') || []
|
const filterList = config.get("notificationFilterList") || [];
|
||||||
const sessionId = data.sessionId
|
const sessionId = data.sessionId;
|
||||||
|
|
||||||
if (sessionId && filterMode !== 'all' && filterList.length > 0) {
|
if (sessionId && filterMode !== "all" && filterList.length > 0) {
|
||||||
const isInList = filterList.includes(sessionId)
|
const isInList = filterList.includes(sessionId);
|
||||||
if (filterMode === 'whitelist' && !isInList) {
|
if (filterMode === "whitelist" && !isInList) {
|
||||||
// 白名单模式:不在列表中则不显示
|
// 白名单模式:不在列表中则不显示
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (filterMode === 'blacklist' && isInList) {
|
if (filterMode === "blacklist" && isInList) {
|
||||||
// 黑名单模式:在列表中则不显示
|
// 黑名单模式:在列表中则不显示
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let win = notificationWindow
|
// Linux 使用 D-Bus 通知
|
||||||
|
if (isLinux) {
|
||||||
|
await showLinuxNotification(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let win = notificationWindow;
|
||||||
if (!win || win.isDestroyed()) {
|
if (!win || win.isDestroyed()) {
|
||||||
win = createNotificationWindow()
|
win = createNotificationWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!win) return
|
if (!win) return;
|
||||||
|
|
||||||
// 确保加载完成
|
// 确保加载完成
|
||||||
if (win.webContents.isLoading()) {
|
if (win.webContents.isLoading()) {
|
||||||
win.once('ready-to-show', () => {
|
win.once("ready-to-show", () => {
|
||||||
showAndSend(win!, data)
|
showAndSend(win!, data);
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
showAndSend(win, data)
|
showAndSend(win, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastNotificationData: any = null
|
// 显示Linux通知
|
||||||
|
async function showLinuxNotification(data: any) {
|
||||||
|
if (!linuxNotificationService) {
|
||||||
|
try {
|
||||||
|
linuxNotificationService =
|
||||||
|
await import("../services/linuxNotificationService");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[NotificationWindow] Failed to load Linux notification service:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { showLinuxNotification: showNotification } = linuxNotificationService;
|
||||||
|
|
||||||
|
const notificationData = {
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
avatarUrl: data.avatarUrl,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
expireTimeout: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
showNotification(notificationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastNotificationData: any = null;
|
||||||
|
|
||||||
async function showAndSend(win: BrowserWindow, data: any) {
|
async function showAndSend(win: BrowserWindow, data: any) {
|
||||||
lastNotificationData = data
|
lastNotificationData = data;
|
||||||
const config = ConfigService.getInstance()
|
const config = ConfigService.getInstance();
|
||||||
const position = (await config.get('notificationPosition')) || 'top-right'
|
const position = (await config.get("notificationPosition")) || "top-right";
|
||||||
|
|
||||||
// 更新位置
|
// 更新位置
|
||||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize
|
const { width: screenWidth, height: screenHeight } =
|
||||||
const winWidth = position === 'top-center' ? 280 : 344
|
screen.getPrimaryDisplay().workAreaSize;
|
||||||
const winHeight = 114
|
const winWidth = position === "top-center" ? 280 : 344;
|
||||||
const padding = 20
|
const winHeight = 114;
|
||||||
|
const padding = 20;
|
||||||
|
|
||||||
let x = 0
|
let x = 0;
|
||||||
let y = 0
|
let y = 0;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'top-center':
|
case "top-center":
|
||||||
x = (screenWidth - winWidth) / 2
|
x = (screenWidth - winWidth) / 2;
|
||||||
y = padding
|
y = padding;
|
||||||
break
|
break;
|
||||||
case 'top-right':
|
case "top-right":
|
||||||
x = screenWidth - winWidth - padding
|
x = screenWidth - winWidth - padding;
|
||||||
y = padding
|
y = padding;
|
||||||
break
|
break;
|
||||||
case 'bottom-right':
|
case "bottom-right":
|
||||||
x = screenWidth - winWidth - padding
|
x = screenWidth - winWidth - padding;
|
||||||
y = screenHeight - winHeight - padding
|
y = screenHeight - winHeight - padding;
|
||||||
break
|
break;
|
||||||
case 'top-left':
|
case "top-left":
|
||||||
x = padding
|
x = padding;
|
||||||
y = padding
|
y = padding;
|
||||||
break
|
break;
|
||||||
case 'bottom-left':
|
case "bottom-left":
|
||||||
x = padding
|
x = padding;
|
||||||
y = screenHeight - winHeight - padding
|
y = screenHeight - winHeight - padding;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
win.setPosition(Math.floor(x), Math.floor(y))
|
win.setPosition(Math.floor(x), Math.floor(y));
|
||||||
win.setSize(winWidth, winHeight) // 确保尺寸
|
win.setSize(winWidth, winHeight); // 确保尺寸
|
||||||
|
|
||||||
// 设为可交互
|
// 设为可交互
|
||||||
win.setIgnoreMouseEvents(false)
|
win.setIgnoreMouseEvents(false);
|
||||||
win.showInactive() // 显示但不聚焦
|
win.showInactive(); // 显示但不聚焦
|
||||||
win.setAlwaysOnTop(true, 'screen-saver') // 最高层级
|
win.setAlwaysOnTop(true, "screen-saver"); // 最高层级
|
||||||
|
|
||||||
win.webContents.send('notification:show', { ...data, position })
|
win.webContents.send("notification:show", { ...data, position });
|
||||||
|
|
||||||
// 自动关闭计时器通常由渲染进程管理
|
// 自动关闭计时器通常由渲染进程管理
|
||||||
// 渲染进程发送 'notification:close' 来隐藏窗口
|
// 渲染进程发送 'notification:close' 来隐藏窗口
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerNotificationHandlers() {
|
// 注册通知处理
|
||||||
ipcMain.handle('notification:show', (_, data) => {
|
export async function registerNotificationHandlers() {
|
||||||
showNotification(data)
|
// Linux: 初始化D-Bus服务
|
||||||
})
|
if (isLinux) {
|
||||||
|
try {
|
||||||
|
const linuxNotificationModule =
|
||||||
|
await import("../services/linuxNotificationService");
|
||||||
|
linuxNotificationService = linuxNotificationModule;
|
||||||
|
|
||||||
ipcMain.handle('notification:close', () => {
|
// 初始化服务
|
||||||
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
await linuxNotificationModule.initLinuxNotificationService();
|
||||||
notificationWindow.hide()
|
|
||||||
notificationWindow.setIgnoreMouseEvents(true, { forward: true })
|
// 在Linux上注册通知点击回调
|
||||||
|
linuxNotificationModule.onNotificationAction((sessionId: string) => {
|
||||||
|
console.log(
|
||||||
|
"[NotificationWindow] Linux notification clicked, sessionId:",
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
// 如果设置了导航处理程序,则使用该处理程序;否则,回退到ipcMain方法。
|
||||||
|
if (onNotificationNavigate) {
|
||||||
|
onNotificationNavigate(sessionId);
|
||||||
|
} else {
|
||||||
|
// 如果尚未设置处理程序,则通过ipcMain发出事件
|
||||||
|
// 正常流程中不应该发生这种情况,因为我们在初始化之前设置了处理程序。
|
||||||
|
console.warn(
|
||||||
|
"[NotificationWindow] onNotificationNavigate not set yet",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[NotificationWindow] Linux notification service initialized",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[NotificationWindow] Failed to initialize Linux notification service:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("notification:show", (_, data) => {
|
||||||
|
showNotification(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("notification:close", () => {
|
||||||
|
if (isLinux && linuxNotificationService) {
|
||||||
|
// 注册通知点击回调函数。Linux通知通过D-Bus自动关闭,但我们可以根据需要进行跟踪
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
||||||
|
notificationWindow.hide();
|
||||||
|
notificationWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle renderer ready event (fix race condition)
|
// Handle renderer ready event (fix race condition)
|
||||||
ipcMain.on('notification:ready', (event) => {
|
ipcMain.on("notification:ready", (event) => {
|
||||||
console.log('[NotificationWindow] Renderer ready, checking cached data')
|
if (isLinux) {
|
||||||
if (lastNotificationData && notificationWindow && !notificationWindow.isDestroyed()) {
|
// Linux不需要通知窗口,拦截通知窗口渲染
|
||||||
console.log('[NotificationWindow] Re-sending cached data')
|
return;
|
||||||
notificationWindow.webContents.send('notification:show', lastNotificationData)
|
|
||||||
}
|
}
|
||||||
})
|
console.log("[NotificationWindow] Renderer ready, checking cached data");
|
||||||
|
if (
|
||||||
|
lastNotificationData &&
|
||||||
|
notificationWindow &&
|
||||||
|
!notificationWindow.isDestroyed()
|
||||||
|
) {
|
||||||
|
console.log("[NotificationWindow] Re-sending cached data");
|
||||||
|
notificationWindow.webContents.send(
|
||||||
|
"notification:show",
|
||||||
|
lastNotificationData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle resize request from renderer
|
// Handle resize request from renderer
|
||||||
ipcMain.on('notification:resize', (event, { width, height }) => {
|
ipcMain.on("notification:resize", (event, { width, height }) => {
|
||||||
|
if (isLinux) {
|
||||||
|
// Linux 通知通过D-Bus自动调整大小
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
||||||
// Enforce max-height if needed, or trust renderer
|
// Enforce max-height if needed, or trust renderer
|
||||||
// Ensure it doesn't go off screen bottom?
|
// Ensure it doesn't go off screen bottom?
|
||||||
@@ -213,12 +322,12 @@ export function registerNotificationHandlers() {
|
|||||||
// If bottom-right, y needs to prevent overflow.
|
// If bottom-right, y needs to prevent overflow.
|
||||||
|
|
||||||
// Ideally we get current config position
|
// Ideally we get current config position
|
||||||
const bounds = notificationWindow.getBounds()
|
const bounds = notificationWindow.getBounds();
|
||||||
// Check if we need to adjust Y?
|
// Check if we need to adjust Y?
|
||||||
// For now, let's just set the size as requested.
|
// For now, let's just set the size as requested.
|
||||||
notificationWindow.setSize(Math.round(width), Math.round(height))
|
notificationWindow.setSize(Math.round(width), Math.round(height));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 'notification-clicked' 在 main.ts 中处理 (导航)
|
// 'notification-clicked' 在 main.ts 中处理 (导航)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vscode/sudo-prompt": "^9.3.2",
|
"@vscode/sudo-prompt": "^9.3.2",
|
||||||
|
"dbus-native": "^0.4.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user