Merge pull request #655 from FATFATHAO/feat/linux-notification

[#654] fix: 更改linux中的消息通知走D-bus总线
This commit is contained in:
H3CoF6
2026-04-07 03:10:50 +08:00
committed by GitHub
5 changed files with 665 additions and 185 deletions

View File

@@ -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(
{ {

View 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
View 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;
}

View File

@@ -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 中处理 (导航)
} }

View File

@@ -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",