重新修复 #654 所提到的问题

This commit is contained in:
cc
2026-04-07 20:14:23 +08:00
parent 5bc46fadfc
commit 0acad9927a
4 changed files with 97 additions and 419 deletions

View File

@@ -1,12 +1,8 @@
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";
import { app, Notification } from "electron";
export interface LinuxNotificationData {
sessionId?: string;
@@ -18,26 +14,29 @@ export interface LinuxNotificationData {
type NotificationCallback = (sessionId: string) => void;
let sessionBus: dbus.DBusConnection | null = null;
let notificationCallbacks: NotificationCallback[] = [];
let pendingNotifications: Map<number, LinuxNotificationData> = new Map();
let notificationCounter = 1;
const activeNotifications: Map<number, Notification> = new Map();
const closeTimers: Map<number, NodeJS.Timeout> = 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();
function nextNotificationId(): number {
const id = notificationCounter;
notificationCounter += 1;
return id;
}
// 挂载底层socket的error事件防止掉线即可
sessionBus.connection.on("error", (err: Error) => {
console.error("[LinuxNotification] D-Bus connection error:", err);
sessionBus = null; // 报错清理死对象
});
function clearNotificationState(notificationId: number): void {
activeNotifications.delete(notificationId);
const timer = closeTimers.get(notificationId);
if (timer) {
clearTimeout(timer);
closeTimers.delete(notificationId);
}
return sessionBus;
}
// 确保缓存目录存在
@@ -125,66 +124,76 @@ async function downloadAvatarToLocal(url: string): Promise<string | null> {
}
}
function triggerNotificationCallback(sessionId: string): void {
for (const callback of notificationCallbacks) {
try {
callback(sessionId);
} catch (error) {
console.error("[LinuxNotification] Callback error:", error);
}
}
}
export async function showLinuxNotification(
data: LinuxNotificationData,
): Promise<number | null> {
if (process.platform !== "linux") {
return null;
}
if (!Notification.isSupported()) {
console.warn("[LinuxNotification] Notification API is not supported");
return null;
}
try {
const bus = await getSessionBus();
const appName = "WeFlow";
const replaceId = 0;
const expireTimeout = data.expireTimeout ?? 5000;
// 处理头像下载到本地或使用URL
let appIcon = "";
let hints: any[] = [];
let iconPath: string | undefined;
if (data.avatarUrl) {
// 优先尝试下载到本地
const localPath = await downloadAvatarToLocal(data.avatarUrl);
if (localPath) {
hints = [["image-path", ["s", localPath]]];
}
iconPath = (await downloadAvatarToLocal(data.avatarUrl)) || undefined;
}
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);
},
);
const notification = new Notification({
title: data.title,
body: data.content,
icon: iconPath,
});
const notificationId = nextNotificationId();
activeNotifications.set(notificationId, notification);
notification.on("click", () => {
if (data.sessionId) {
triggerNotificationCallback(data.sessionId);
}
});
notification.on("close", () => {
clearNotificationState(notificationId);
});
notification.on("failed", (_, error) => {
console.error("[LinuxNotification] Notification failed:", error);
clearNotificationState(notificationId);
});
const expireTimeout = data.expireTimeout ?? 5000;
if (expireTimeout > 0) {
const timer = setTimeout(() => {
const currentNotification = activeNotifications.get(notificationId);
if (currentNotification) {
currentNotification.close();
}
}, expireTimeout);
closeTimers.set(notificationId, timer);
}
notification.show();
console.log(
`[LinuxNotification] Shown notification ${notificationId}: ${data.title}`,
);
return notificationId;
} catch (error) {
console.error("[LinuxNotification] Failed to show notification:", error);
return null;
@@ -194,59 +203,22 @@ export async function showLinuxNotification(
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);
}
const notification = activeNotifications.get(notificationId);
if (!notification) return;
notification.close();
clearNotificationState(notificationId);
}
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);
if (process.platform !== "linux") {
return [];
}
if (!Notification.isSupported()) {
return [];
}
return ["native-notification", "click"];
}
export function onNotificationAction(callback: NotificationCallback): void {
@@ -262,83 +234,17 @@ export function removeNotificationCallback(
}
}
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);
if (!Notification.isSupported()) {
console.warn("[LinuxNotification] Notification API is not supported");
return;
}
const caps = await getCapabilities();
console.log("[LinuxNotification] Service initialized with native API:", caps);
}