diff --git a/eslint.config.cjs b/eslint.config.cjs index 771c36d..10e7284 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -40,7 +40,10 @@ module.exports = [ { files: ['**/*.{ts,tsx}'], rules: { - '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' } + ], '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'warn' } diff --git a/package.json b/package.json index 15213b9..b748f09 100644 --- a/package.json +++ b/package.json @@ -30,23 +30,16 @@ "build:linux:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --linux" }, "dependencies": { - "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", - "@heroui/react": "^2.8.5", "@mihomo-party/sysproxy": "^2.0.8", - "@types/crypto-js": "^4.2.2", "adm-zip": "^0.5.16", "axios": "^1.13.2", - "chart.js": "^4.5.1", "chokidar": "^4.0.3", "croner": "^9.1.0", "crypto-js": "^4.2.0", - "dayjs": "^1.11.19", "express": "^5.1.0", "i18next": "^25.6.2", "iconv-lite": "^0.6.3", - "react-chartjs-2": "^5.3.1", - "react-i18next": "^15.7.4", "webdav": "^5.8.0", "ws": "^8.18.3", "yaml": "^2.8.1" @@ -58,8 +51,10 @@ "@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@heroui/react": "^2.8.5", "@tailwindcss/vite": "^4.1.17", "@types/adm-zip": "^0.5.7", + "@types/crypto-js": "^4.2.2", "@types/express": "^5.0.5", "@types/node": "^24.10.1", "@types/pubsub-js": "^1.8.6", @@ -67,7 +62,9 @@ "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^4.7.0", + "chart.js": "^4.5.1", "cron-validator": "^1.4.0", + "dayjs": "^1.11.19", "driver.js": "^1.3.6", "electron": "^37.10.0", "electron-builder": "26.0.12", @@ -86,8 +83,10 @@ "prettier": "^3.6.2", "pubsub-js": "^1.9.5", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", + "react-i18next": "^15.7.4", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-monaco-editor": "^0.59.0", diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 26ca45e..215ab73 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -562,7 +562,7 @@ async function waitForCoreReady(): Promise { await axios.get('/') await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`) return - } catch (error) { + } catch { if (i === 0) { await managerLogger.info('Waiting for core to be ready...') } @@ -791,7 +791,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise { if (processJson.Name.includes('mihomo') && processJson.Path === null) { return true } - } catch (error) { + } catch { await managerLogger.info( `Cannot get info for process ${pid}, might be high privilege` ) @@ -835,7 +835,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise { } } } - } catch (error) { + } catch { // ignore } } diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index 94c3048..56fb167 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -15,7 +15,7 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(item) - } catch (e) { + } catch { /* ignore */ } }, @@ -26,7 +26,7 @@ export async function initProfileUpdater(): Promise { intervalPool[item.id] = new Cron(item.interval, async () => { try { await addProfileItem(item) - } catch (e) { + } catch { /* ignore */ } }) @@ -34,7 +34,7 @@ export async function initProfileUpdater(): Promise { try { await addProfileItem(item) - } catch (e) { + } catch { /* ignore */ } } @@ -46,7 +46,7 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(currentItem) - } catch (e) { + } catch { /* ignore */ } }, @@ -57,7 +57,7 @@ export async function initProfileUpdater(): Promise { async () => { try { await addProfileItem(currentItem) - } catch (e) { + } catch { /* ignore */ } }, @@ -67,7 +67,7 @@ export async function initProfileUpdater(): Promise { intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => { try { await addProfileItem(currentItem) - } catch (e) { + } catch { /* ignore */ } }) @@ -75,7 +75,7 @@ export async function initProfileUpdater(): Promise { try { await addProfileItem(currentItem) - } catch (e) { + } catch { /* ignore */ } } @@ -96,7 +96,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise { async () => { try { await addProfileItem(item) - } catch (e) { + } catch { /* ignore */ } }, @@ -106,7 +106,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise { intervalPool[item.id] = new Cron(item.interval, async () => { try { await addProfileItem(item) - } catch (e) { + } catch { /* ignore */ } }) diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index 7a86f75..9938b88 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -120,7 +120,7 @@ export const buildContextMenu = async (): Promise => { groupsMenu = groupItems groupsMenu.unshift({ type: 'separator' }) } - } catch (e) { + } catch { // ignore // 避免出错时无法创建托盘菜单 } @@ -206,7 +206,7 @@ export const buildContextMenu = async (): Promise => { await patchAppConfig({ sysProxy: { enable } }) mainWindow?.webContents.send('appConfigUpdated') floatingWindow?.webContents.send('appConfigUpdated') - } catch (e) { + } catch { // ignore } finally { ipcMain.emit('updateTrayMenu') diff --git a/src/main/sys/autoRun.ts b/src/main/sys/autoRun.ts index f84d037..0c4a785 100644 --- a/src/main/sys/autoRun.ts +++ b/src/main/sys/autoRun.ts @@ -61,7 +61,7 @@ export async function checkAutoRun(): Promise { `chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"` ) return stdout.includes(appName) - } catch (e) { + } catch { return false } } @@ -96,7 +96,7 @@ export async function enableAutoRun(): Promise { await execPromise( `powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"` ) - } catch (e) { + } catch { await managerLogger.info('Maybe the user rejected the UAC dialog?') } } @@ -144,7 +144,7 @@ export async function disableAutoRun(): Promise { await execPromise( `powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"` ) - } catch (e) { + } catch { await managerLogger.info('Maybe the user rejected the UAC dialog?') } } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 8678c8a..0189638 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,6 +1,22 @@ -import { ElectronAPI } from '@electron-toolkit/preload' import { webUtils } from 'electron' +type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void + +interface SafeIpcRenderer { + invoke: (channel: string, ...args: unknown[]) => Promise + send: (channel: string, ...args: unknown[]) => void + on: (channel: string, listener: IpcListener) => void + removeListener: (channel: string, listener: IpcListener) => void + removeAllListeners: (channel: string) => void +} + +interface ElectronAPI { + ipcRenderer: SafeIpcRenderer + process: { + platform: NodeJS.Platform + } +} + declare global { interface Window { electron: ElectronAPI diff --git a/src/preload/index.ts b/src/preload/index.ts index 46da136..bc01309 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,13 +1,231 @@ -import { contextBridge, webUtils } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' +import { contextBridge, ipcRenderer, webUtils } from 'electron' + +// 允许的 invoke channels 白名单 +const validInvokeChannels = [ + // Mihomo API + 'mihomoVersion', + 'mihomoCloseConnection', + 'mihomoCloseAllConnections', + 'mihomoRules', + 'mihomoProxies', + 'mihomoGroups', + 'mihomoProxyProviders', + 'mihomoUpdateProxyProviders', + 'mihomoRuleProviders', + 'mihomoUpdateRuleProviders', + 'mihomoChangeProxy', + 'mihomoUnfixedProxy', + 'mihomoUpgradeGeo', + 'mihomoUpgrade', + 'mihomoUpgradeUI', + 'mihomoUpgradeConfig', + 'mihomoProxyDelay', + 'mihomoGroupDelay', + 'patchMihomoConfig', + 'mihomoSmartGroupWeights', + 'mihomoSmartFlushCache', + // AutoRun + 'checkAutoRun', + 'enableAutoRun', + 'disableAutoRun', + // Config + 'getAppConfig', + 'patchAppConfig', + 'getControledMihomoConfig', + 'patchControledMihomoConfig', + 'resetAppConfig', + // Profile + 'getProfileConfig', + 'setProfileConfig', + 'getCurrentProfileItem', + 'getProfileItem', + 'getProfileStr', + 'setProfileStr', + 'addProfileItem', + 'removeProfileItem', + 'updateProfileItem', + 'changeCurrentProfile', + 'addProfileUpdater', + 'removeProfileUpdater', + // Override + 'getOverrideConfig', + 'setOverrideConfig', + 'getOverrideItem', + 'addOverrideItem', + 'removeOverrideItem', + 'updateOverrideItem', + 'getOverride', + 'setOverride', + // File + 'getFileStr', + 'setFileStr', + 'convertMrsRuleset', + 'getRuntimeConfig', + 'getRuntimeConfigStr', + 'getSmartOverrideContent', + 'getRuleStr', + 'setRuleStr', + 'getFilePath', + 'readTextFile', + 'openFile', + // Core + 'restartCore', + 'startMonitor', + 'quitWithoutCore', + // System + 'triggerSysProxy', + 'checkTunPermissions', + 'grantTunPermissions', + 'manualGrantCorePermition', + 'checkAdminPrivileges', + 'restartAsAdmin', + 'checkMihomoCorePermissions', + 'requestTunPermissions', + 'checkHighPrivilegeCore', + 'showTunPermissionDialog', + 'showErrorDialog', + 'openUWPTool', + 'setupFirewall', + 'getInterfaces', + 'setNativeTheme', + 'copyEnv', + // Update + 'checkUpdate', + 'downloadAndInstallUpdate', + 'getVersion', + 'platform', + 'fetchMihomoTags', + 'installSpecificMihomoCore', + 'clearMihomoVersionCache', + // Backup + 'webdavBackup', + 'webdavRestore', + 'listWebdavBackups', + 'webdavDelete', + 'reinitWebdavBackupScheduler', + 'exportLocalBackup', + 'importLocalBackup', + // SubStore + 'startSubStoreFrontendServer', + 'stopSubStoreFrontendServer', + 'startSubStoreBackendServer', + 'stopSubStoreBackendServer', + 'downloadSubStore', + 'subStorePort', + 'subStoreFrontendPort', + 'subStoreSubs', + 'subStoreCollections', + // Theme + 'resolveThemes', + 'fetchThemes', + 'importThemes', + 'readTheme', + 'writeTheme', + 'applyTheme', + // Tray + 'showTrayIcon', + 'closeTrayIcon', + 'updateTrayIcon', + 'updateTrayIconImmediate', + // Window + 'showMainWindow', + 'closeMainWindow', + 'triggerMainWindow', + 'showFloatingWindow', + 'closeFloatingWindow', + 'showContextMenu', + 'setTitleBarOverlay', + 'setAlwaysOnTop', + 'isAlwaysOnTop', + 'openDevTools', + 'createHeapSnapshot', + 'relaunchApp', + 'quitApp', + // Shortcut + 'registerShortcut', + // Misc + 'getGistUrl', + 'getImageDataURL', + 'changeLanguage' +] as const + +// 允许的 on/removeListener channels 白名单 +const validListenChannels = [ + 'mihomoLogs', + 'mihomoConnections', + 'mihomoTraffic', + 'mihomoMemory', + 'appConfigUpdated', + 'controledMihomoConfigUpdated', + 'profileConfigUpdated', + 'groupsUpdated', + 'rulesUpdated' +] as const + +// 允许的 send channels 白名单 +const validSendChannels = [ + 'updateTrayMenu', + 'updateFloatingWindow', + 'trayIconUpdate' +] as const + +type InvokeChannel = (typeof validInvokeChannels)[number] +type ListenChannel = (typeof validListenChannels)[number] +type SendChannel = (typeof validSendChannels)[number] + +type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void +const listenerMap = new Map>() + +// 安全的 IPC API,只暴露白名单内的 channels +const electronAPI = { + ipcRenderer: { + invoke: (channel: InvokeChannel, ...args: unknown[]): Promise => { + if (validInvokeChannels.includes(channel)) { + return ipcRenderer.invoke(channel, ...args) + } + return Promise.reject(new Error(`Invalid invoke channel: ${channel}`)) + }, + send: (channel: SendChannel, ...args: unknown[]): void => { + if (validSendChannels.includes(channel)) { + ipcRenderer.send(channel, ...args) + } + }, + on: (channel: ListenChannel, listener: IpcListener): void => { + if (validListenChannels.includes(channel)) { + if (!listenerMap.has(channel)) { + listenerMap.set(channel, new Set()) + } + listenerMap.get(channel)!.add(listener) + ipcRenderer.on(channel, listener) + } + }, + removeListener: (channel: ListenChannel, listener: IpcListener): void => { + if (validListenChannels.includes(channel)) { + listenerMap.get(channel)?.delete(listener) + ipcRenderer.removeListener(channel, listener) + } + }, + removeAllListeners: (channel: ListenChannel): void => { + if (validListenChannels.includes(channel)) { + const listeners = listenerMap.get(channel) + if (listeners) { + listeners.forEach((listener) => { + ipcRenderer.removeListener(channel, listener) + }) + listeners.clear() + } + } + } + }, + process: { + platform: process.platform + } +} -// Custom APIs for renderer const api = { webUtils: webUtils } -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. + if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 0aaed52..e6e7460 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -86,7 +86,7 @@ const App: React.FC = () => { options.color = window.getComputedStyle(document.documentElement).backgroundColor options.symbolColor = window.getComputedStyle(document.documentElement).color setTitleBarOverlay(options) - } catch (e) { + } catch { // ignore } } diff --git a/src/renderer/src/FloatingApp.tsx b/src/renderer/src/FloatingApp.tsx index 5d07f63..64fc4b3 100644 --- a/src/renderer/src/FloatingApp.tsx +++ b/src/renderer/src/FloatingApp.tsx @@ -49,7 +49,8 @@ const FloatingApp: React.FC = () => { }, [spinSpeed, spinFloatingIcon]) useEffect(() => { - window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => { + window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => { + const info = args[0] as IMihomoTrafficInfo setUpload(info.up) setDownload(info.down) }) diff --git a/src/renderer/src/components/base/base-page.tsx b/src/renderer/src/components/base/base-page.tsx index 53f4879..bce32c4 100644 --- a/src/renderer/src/components/base/base-page.tsx +++ b/src/renderer/src/components/base/base-page.tsx @@ -32,7 +32,7 @@ const BasePage = forwardRef((props, ref) => { // @ts-ignore windowControlsOverlay const windowControlsOverlay = window.navigator.windowControlsOverlay setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width) - } catch (e) { + } catch { // ignore } } diff --git a/src/renderer/src/components/profiles/edit-info-modal.tsx b/src/renderer/src/components/profiles/edit-info-modal.tsx index 53f1ae4..be6cbe0 100644 --- a/src/renderer/src/components/profiles/edit-info-modal.tsx +++ b/src/renderer/src/components/profiles/edit-info-modal.tsx @@ -149,7 +149,7 @@ const EditInfoModal: React.FC = (props) => { // 非纯数字 try { setValues({ ...values, interval: v }) - } catch (e) { + } catch { // ignore } } diff --git a/src/renderer/src/components/resources/viewer.tsx b/src/renderer/src/components/resources/viewer.tsx index e42369b..b280e32 100644 --- a/src/renderer/src/components/resources/viewer.tsx +++ b/src/renderer/src/components/resources/viewer.tsx @@ -66,7 +66,7 @@ const Viewer: React.FC = (props) => { }) ) } - } catch (error) { + } catch { setCurrData(fileContent) } } finally { diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index d5b652e..29f2e4b 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -126,7 +126,8 @@ const ConnCard: React.FC = (props) => { const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null useEffect(() => { - window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => { + window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => { + const info = args[0] as IMihomoTrafficInfo setUpload(info.up) setDownload(info.down) const data = series diff --git a/src/renderer/src/components/sider/mihomo-core-card.tsx b/src/renderer/src/components/sider/mihomo-core-card.tsx index dea4d9f..af5428c 100644 --- a/src/renderer/src/components/sider/mihomo-core-card.tsx +++ b/src/renderer/src/components/sider/mihomo-core-card.tsx @@ -43,7 +43,8 @@ const MihomoCoreCard: React.FC = (props) => { const token = PubSub.subscribe('mihomo-core-changed', () => { mutate() }) - window.electron.ipcRenderer.on('mihomoMemory', (_e, info: IMihomoMemoryInfo) => { + window.electron.ipcRenderer.on('mihomoMemory', (_e, ...args) => { + const info = args[0] as IMihomoMemoryInfo setMem(info.inuse) }) return (): void => { diff --git a/src/renderer/src/pages/connections.tsx b/src/renderer/src/pages/connections.tsx index f2f6072..126f02c 100644 --- a/src/renderer/src/pages/connections.tsx +++ b/src/renderer/src/pages/connections.tsx @@ -170,7 +170,8 @@ const Connections: React.FC = () => { } useEffect(() => { - const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => { + const handler = (_e: unknown, ...args: unknown[]): void => { + const info = args[0] as IMihomoConnectionsInfo setConnectionsInfo(info) if (!info.connections) return diff --git a/src/renderer/src/pages/logs.tsx b/src/renderer/src/pages/logs.tsx index 555d10a..c878131 100644 --- a/src/renderer/src/pages/logs.tsx +++ b/src/renderer/src/pages/logs.tsx @@ -26,7 +26,8 @@ const cachedLogs: { } } -window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => { +window.electron.ipcRenderer.on('mihomoLogs', (_e, ...args) => { + const log = args[0] as IMihomoLogInfo log.time = new Date().toLocaleString() cachedLogs.log.push(log) if (cachedLogs.log.length >= 500) { diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 4fcb6ca..a615514 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -1,8 +1,8 @@ import { TitleBarOverlayOptions } from 'electron' -function checkIpcError(response: T | { invokeError: unknown }): T { +function checkIpcError(response: unknown): T { if (response && typeof response === 'object' && 'invokeError' in response) { - throw response.invokeError + throw (response as { invokeError: unknown }).invokeError } return response as T }