Files
nl-admin-view/apps/desktop/src/main/ipc.ts
李琦 318aeab092
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
AI代码生成工具、个人中心
2026-08-13 19:04:28 +08:00

224 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* IPC 通道注册:渲染层 window.desktopAPI 各能力的主进程实现
* 安全约定:所有 handler 统一先校验 sender 来源app://local 或本地 dev server
* 防止意外加载的第三方页面调用桌面能力
*/
import type { IpcMainInvokeEvent } from 'electron';
import {
app,
BrowserWindow,
ipcMain,
nativeImage,
Notification,
} from 'electron';
import log from 'electron-log/main';
import { PRODUCT_NAME } from './config';
import { SETTING_KEYS, settingsStore } from './store';
import { refreshTrayMenu } from './tray';
import { checkUpdateManually, quitAndInstallNow } from './updater';
import {
getAutoLaunchEnabled,
getResourcePath,
setAutoLaunchEnabled,
} from './utils';
import {
getMainWindow,
isTrustedWebContentsUrl,
showMainWindow,
} from './window';
/** 渲染层 notify() 的入参结构(与 preload 保持同步) */
interface NotifyOptions {
body?: string;
flash?: boolean;
route?: string;
silent?: boolean;
title: string;
}
/** 渲染层 printHTML() 的入参结构(与 preload 保持同步) */
interface PrintHTMLOptions {
deviceName?: string;
html: string;
landscape?: boolean;
silent?: boolean;
}
/**
* 带来源校验的 ipcMain.handle 包装
* 为什么不用裸 ipcMain.handle每个通道都要校验 sender包一层避免每处复制粘贴校验代码
*/
function handle(
channel: string,
handler: (event: IpcMainInvokeEvent, ...args: any[]) => unknown,
): void {
ipcMain.handle(channel, (event, ...args) => {
const senderUrl = event.senderFrame?.url ?? '';
if (!isTrustedWebContentsUrl(senderUrl)) {
log.warn('[ipc] 拒绝不可信来源的 IPC 调用:', channel, senderUrl);
throw new Error('IPC 调用来源不可信');
}
return handler(event, ...args);
});
}
/** 注册全部 IPC 通道,应用启动时调用一次 */
export function registerIpcHandlers(): void {
handle('desktop:get-version', () => app.getVersion());
// ---- 系统通知 ----
handle('desktop:notify', (_event, options: NotifyOptions) => {
showNotification(options);
});
// ---- 未读角标dataUrl 由 preload 的 canvas 绘制,仅 Windows 用到) ----
handle('desktop:set-badge', (_event, count: number, dataUrl: string) => {
setBadge(count, dataUrl);
});
// ---- 任务栏闪烁 ----
handle('desktop:flash-frame', (_event, flag: boolean) => {
const win = getMainWindow();
if (!win) return;
// 已聚焦时闪烁没有意义还会造成任务栏图标闪一下的视觉bug
if (flag && win.isFocused()) return;
win.flashFrame(!!flag);
});
// ---- 开机自启 ----
handle('desktop:get-auto-launch', () => getAutoLaunchEnabled());
handle('desktop:set-auto-launch', (_event, enabled: boolean) => {
const result = setAutoLaunchEnabled(!!enabled);
// 渲染层改了自启后,托盘菜单的勾选状态要同步
refreshTrayMenu();
return result;
});
// ---- 静默打印 ----
handle('desktop:get-printers', async () => {
const win = getMainWindow();
return win ? await win.webContents.getPrintersAsync() : [];
});
handle('desktop:print-html', (_event, options: PrintHTMLOptions) =>
printHTML(options),
);
// ---- 桌面端本地设置 ----
handle('desktop:get-setting', (_event, key: string) => settingsStore.get(key));
handle('desktop:set-setting', (_event, key: string, value: unknown) => {
settingsStore.set(key, value);
});
// ---- 更新 ----
handle('desktop:check-update', () => checkUpdateManually());
handle('desktop:quit-and-install', () => quitAndInstallNow());
}
/**
* 弹系统通知:点击唤起主窗口,携带 route 时再通知渲染层跳转站内路由
* Windows 能弹的前提app.setAppUserModelId 与安装包 appId 一致(入口处已设置)
*/
function showNotification(options: NotifyOptions): void {
if (!Notification.isSupported()) {
log.warn('[ipc] 当前系统不支持通知');
return;
}
const notification = new Notification({
body: options.body ?? '',
icon: nativeImage.createFromPath(getResourcePath('icon.png')),
silent: options.silent ?? false,
title: options.title || PRODUCT_NAME,
});
notification.on('click', () => {
showMainWindow();
if (options.route) {
getMainWindow()?.webContents.send('desktop:navigate', options.route);
}
});
notification.show();
// 默认行为窗口未聚焦时同步闪烁任务栏mac 表现为 Dock 弹跳),聚焦后自动清除
const win = getMainWindow();
if ((options.flash ?? true) && win && !win.isFocused()) {
win.flashFrame(true);
}
}
/**
* 设置未读角标,按平台分支:
* mac 用 Dock 原生数字角标Windows 用 preload 画好的红底数字图做任务栏覆盖图;
* Linux 尽力而为走 setBadgeCount仅部分桌面环境支持
*/
function setBadge(count: number, dataUrl: string): void {
const normalized = Math.max(0, Math.floor(count) || 0);
if (process.platform === 'darwin') {
app.dock?.setBadge(normalized > 0 ? String(normalized) : '');
return;
}
if (process.platform === 'win32') {
const win = getMainWindow();
if (!win) return;
if (normalized > 0 && dataUrl) {
win.setOverlayIcon(
nativeImage.createFromDataURL(dataUrl),
`${normalized} 条未读`,
);
} else {
win.setOverlayIcon(null, '');
}
return;
}
app.setBadgeCount(normalized);
}
/**
* 打印一段完整 HTML
* 实现:隐藏窗口加载 HTMLbase64 data URL 避免特殊字符截断)→ webContents.print
* 静默打印必须有明确的打印机名(入参或本地设置),否则自动降级为弹系统打印对话框
*/
async function printHTML(
options: PrintHTMLOptions,
): Promise<{ message?: string; success: boolean }> {
const deviceName =
options.deviceName ||
(settingsStore.get(SETTING_KEYS.printerDeviceName) as string | undefined);
const silent = (options.silent ?? true) && !!deviceName;
const printWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
try {
const dataUrl = `data:text/html;charset=utf-8;base64,${Buffer.from(
options.html,
'utf8',
).toString('base64')}`;
await printWindow.loadURL(dataUrl);
return await new Promise((resolvePrint) => {
printWindow.webContents.print(
{
deviceName: silent ? deviceName : undefined,
landscape: options.landscape ?? false,
printBackground: true,
silent,
},
(success, failureReason) => {
resolvePrint({
message: success ? undefined : failureReason,
success,
});
},
);
});
} catch (error) {
log.error('[ipc] 打印失败:', error);
return { message: String(error), success: false };
} finally {
if (!printWindow.isDestroyed()) printWindow.destroy();
}
}