diff --git a/package.json b/package.json index 3d5fe51..eb6e44f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-icons": "^5.2.1", "react-router-dom": "^6.25.1", "swr": "^2.2.5", + "ws": "^8.18.0", "yaml": "^2.5.0" }, "devDependencies": { @@ -38,6 +39,7 @@ "@types/node": "^22.0.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/ws": "^8.5.12", "@vitejs/plugin-react": "^4.3.1", "adm-zip": "^0.5.14", "autoprefixer": "^10.4.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0683ae4..6b1dca6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: swr: specifier: ^2.2.5 version: 2.2.5(react@18.3.1) + ws: + specifier: ^8.18.0 + version: 8.18.0 yaml: specifier: ^2.5.0 version: 2.5.0 @@ -60,6 +63,9 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 + '@types/ws': + specifier: ^8.5.12 + version: 8.5.12 '@vitejs/plugin-react': specifier: ^4.3.1 version: 4.3.1(vite@5.3.5(@types/node@22.0.0)) @@ -1746,6 +1752,9 @@ packages: '@types/verror@1.10.10': resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -4170,6 +4179,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -6740,6 +6761,10 @@ snapshots: '@types/verror@1.10.10': optional: true + '@types/ws@8.5.12': + dependencies: + '@types/node': 22.0.0 + '@types/yauzl@2.10.3': dependencies: '@types/node': 22.0.0 @@ -9561,6 +9586,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xmlbuilder@15.1.1: {} y18n@5.0.8: {} diff --git a/src/main/cmds.ts b/src/main/cmds.ts index c0b481a..7c55e5e 100644 --- a/src/main/cmds.ts +++ b/src/main/cmds.ts @@ -1,5 +1,11 @@ import { ipcMain } from 'electron' -import { mihomoConfig, mihomoVersion, patchMihomoConfig } from './mihomo-api' +import { + mihomoConfig, + mihomoConnections, + mihomoRules, + mihomoVersion, + patchMihomoConfig +} from './mihomo-api' import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun' import { getAppConfig, @@ -13,6 +19,8 @@ import { restartCore } from './manager' export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoConfig', mihomoConfig) + ipcMain.handle('mihomoConnections', mihomoConnections) + ipcMain.handle('mihomeRules', mihomoRules) ipcMain.handle('patchMihomoConfig', async (_e, patch) => await patchMihomoConfig(patch)) ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('enableAutoRun', enableAutoRun) diff --git a/src/main/index.ts b/src/main/index.ts index 830ed5d..b5d4f46 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,9 +7,9 @@ import { registerIpcMainHandlers } from './cmds' import { initConfig, appConfig, controledMihomoConfig, setControledMihomoConfig } from './config' import { stopCore, startCore } from './manager' import { initDirs } from './dirs' -import { patchMihomoConfig } from './mihomo-api' +import { mihomoTraffic, patchMihomoConfig } from './mihomo-api' -let window: BrowserWindow | null = null +export let window: BrowserWindow | null = null let tray: Tray | null = null let trayContextMenu: Menu | null = null @@ -57,7 +57,7 @@ if (!gotTheLock) { registerIpcMainHandlers() createWindow() createTray() - + mihomoTraffic() app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. diff --git a/src/main/mihomo-api.ts b/src/main/mihomo-api.ts index 2e70ba5..033ec3c 100644 --- a/src/main/mihomo-api.ts +++ b/src/main/mihomo-api.ts @@ -1,10 +1,11 @@ import axios, { AxiosInstance } from 'axios' import { controledMihomoConfig } from './config' +import WebSocket from 'ws' +import { window } from '.' let axiosIns: AxiosInstance = null! +let mihomoTrafficWs: WebSocket = null! -/// initialize some information -/// enable force update axiosIns export const getAxios = async (force: boolean = false): Promise => { if (axiosIns && !force) return axiosIns @@ -32,8 +33,34 @@ export const mihomoConfig = async (): Promise => { return instance.get('/configs') as Promise } -/// Update current configs export const patchMihomoConfig = async (patch: Partial): Promise => { const instance = await getAxios() return instance.patch('/configs', patch) } + +export const mihomoConnections = async (): Promise => { + const instance = await getAxios() + return instance.get('/connections') as Promise +} + +export const mihomoRules = async (): Promise => { + const instance = await getAxios() + return instance.get('/rules') as Promise +} + +export const mihomoTraffic = (): void => { + let server = controledMihomoConfig['external-controller'] + const secret = controledMihomoConfig.secret ?? '' + if (server?.startsWith(':')) server = `127.0.0.1${server}` + + mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?secret=${secret}`) + + mihomoTrafficWs.onmessage = (e: { data: string }): void => { + window?.webContents.send('mihomoTraffic', JSON.parse(e.data) as IMihomoTrafficInfo) + } + + mihomoTrafficWs.onerror = (): void => { + console.error('Traffic ws error') + mihomoConfig() + } +} diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index e018727..7e5cfea 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -1,11 +1,27 @@ -import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' -import { IoLink } from 'react-icons/io5' +import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { useLocation, useNavigate } from 'react-router-dom' +import { IoLink } from 'react-icons/io5' +import { useEffect } from 'react' +import useSWR from 'swr' +import { mihomoConnections } from '@renderer/utils/ipc' const ConnCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() + const { data: connections } = useSWR('/connections', mihomoConnections, { + refreshInterval: 5000 + }) + + useEffect(() => { + window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => { + console.log(info) + }) + return (): void => { + window.electron.ipcRenderer.removeAllListeners('mihomoTraffic') + } + }, []) + return ( { > + + {connections?.connections?.length ?? 0} + diff --git a/src/renderer/src/components/sider/rule-card.tsx b/src/renderer/src/components/sider/rule-card.tsx index ed9e3f8..983eb31 100644 --- a/src/renderer/src/components/sider/rule-card.tsx +++ b/src/renderer/src/components/sider/rule-card.tsx @@ -1,11 +1,17 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' +import { mihomoRules } from '@renderer/utils/ipc' import { IoGitNetwork } from 'react-icons/io5' import { useLocation, useNavigate } from 'react-router-dom' +import useSWR from 'swr' const RuleCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() + const { data: rules } = useSWR('/connections', mihomoRules, { + refreshInterval: 5000 + }) + return ( { - 1103 + {rules?.rules?.length ?? 0} diff --git a/src/renderer/src/utils/calc.ts b/src/renderer/src/utils/calc.ts new file mode 100644 index 0000000..023d5c9 --- /dev/null +++ b/src/renderer/src/utils/calc.ts @@ -0,0 +1,6 @@ +export function calcTraffic(bit: number): string { + if (bit < 1024) return `${bit} B` + if (bit < 1024 * 1024) return `${(bit / 1024).toFixed(2)} KB` + if (bit < 1024 * 1024 * 1024) return `${(bit / 1024 / 1024).toFixed(2)} MB` + return `${(bit / 1024 / 1024 / 1024).toFixed(2)} GB` +} diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index dba279c..0202c5b 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -6,6 +6,14 @@ export async function mihomoConfig(): Promise { return await window.electron.ipcRenderer.invoke('mihomoConfig') } +export async function mihomoConnections(): Promise { + return await window.electron.ipcRenderer.invoke('mihomoConnections') +} + +export async function mihomoRules(): Promise { + return await window.electron.ipcRenderer.invoke('mihomoRules') +} + export async function patchMihomoConfig(patch: Partial): Promise { await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch) } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 92c6f0e..fa684e0 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -6,6 +6,63 @@ interface IMihomoVersion { meta: boolean } +interface IMihomoTrafficInfo { + up: number + down: number +} + +interface IMihomoRulesInfo { + rules: IMihomoRulesDetail[] +} + +interface IMihomoRulesDetail { + type: string + payload: string + proxy: string + size: number +} + +interface IMihomoConnectionsInfo { + downloadTotal: number + uploadTotal: number + connections?: IMihomoConnectionDetail[] + memory: number +} + +interface IMihomoConnectionDetail { + id: string + metadata: { + network: 'tcp' | 'udp' + type: string + sourceIP: string + destinationIP: string + destinationGeoIP: string + destinationIPASN: string + sourcePort: string + destinationPort: string + inboundIP: string + inboundPort: string + inboundName: string + inboundUser: string + host: string + dnsMode: string + uid: number + process: string + processPath: string + specialProxy: string + specialRules: string + remoteDestination: string + dscp: number + sniffHost: string + } + upload: number + download: number + start: string + chains: string[] + rule: string + rulePayload: string +} + interface IAppConfig { core: 'mihomo' | 'mihomo-alpha' silentStart: boolean