mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-08 07:25:51 +00:00
重新修复 #654 所提到的问题
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user