trafficInfo

This commit is contained in:
pompurin404 2024-08-02 11:25:43 +08:00
parent 945ab611fe
commit 27f2f70f1b
No known key found for this signature in database
22 changed files with 195 additions and 122 deletions

View File

@ -1,12 +1,6 @@
export { appConfig, getAppConfig, setAppConfig } from './app'
export { getAppConfig, setAppConfig } from './app'
export { getControledMihomoConfig, setControledMihomoConfig } from './controledMihomo'
export {
controledMihomoConfig,
getControledMihomoConfig,
setControledMihomoConfig
} from './controledMihomo'
export {
profileConfig,
currentProfile,
getCurrentProfile,
getCurrentProfileItem,
getProfileItem,

View File

@ -1,12 +1,14 @@
import { controledMihomoConfig } from './controledMihomo'
import { getControledMihomoConfig } from './controledMihomo'
import { profileConfigPath, profilePath } from '../utils/dirs'
import { app } from 'electron'
import { restartCore } from '../core/manager'
import { getAppConfig } from './app'
import { window } from '..'
import axios from 'axios'
import yaml from 'yaml'
import fs from 'fs'
export let profileConfig: IProfileConfig // profile.yaml
export let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml
let profileConfig: IProfileConfig // profile.yaml
let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml
export function getProfileConfig(force = false): IProfileConfig {
if (force || !profileConfig) {
@ -16,19 +18,25 @@ export function getProfileConfig(force = false): IProfileConfig {
}
export function getProfileItem(id: string | undefined): IProfileItem {
const items = profileConfig.items
const items = getProfileConfig().items
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' }
}
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item)
profileConfig.items = getProfileConfig().items.filter((item) => item.id !== newItem.id)
profileConfig.items.push(newItem)
console.log(!profileConfig.current)
if (!profileConfig.current) {
let changeProfile = false
if (!getProfileConfig().current) {
profileConfig.current = newItem.id
changeProfile = true
}
console.log(profileConfig.current)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
if (changeProfile) {
getCurrentProfile(true)
restartCore()
}
}
export function removeProfileItem(id: string): void {
@ -37,10 +45,33 @@ export function removeProfileItem(id: string): void {
profileConfig.current = profileConfig.items[0]?.id
}
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
}
export function getCurrentProfileItem(): IProfileItem {
return getProfileItem(profileConfig.current)
return getProfileItem(getProfileConfig().current)
}
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
function parseFilename(str: string): string {
if (str.includes("filename*=UTF-8''")) {
const filename = decodeURIComponent(str.split("filename*=UTF-8''")[1])
return filename
} else {
const filename = str.split('filename=')[1]
return filename
}
}
// subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293
function parseSubinfo(str: string): ISubscriptionUserInfo {
const parts = str.split('; ')
const obj = {} as ISubscriptionUserInfo
parts.forEach((part) => {
const [key, value] = part.split('=')
obj[key] = parseInt(value)
})
return obj
}
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
@ -50,6 +81,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
name: item.name || 'Local File',
type: item.type || 'local',
url: item.url,
interval: item.interval || 0,
updated: new Date().getTime()
} as IProfileItem
switch (newItem.type) {
@ -58,32 +90,31 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
throw new Error('URL is required for remote profile')
}
try {
const ua = getAppConfig().userAgent || 'clash-meta'
const res = await axios.get(item.url, {
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: controledMihomoConfig['mixed-port'] || 7890
port: getControledMihomoConfig()['mixed-port'] || 7890
},
headers: {
'User-Agent': `Mihomo.Party.${app.getVersion()}`
'User-Agent': ua
},
responseType: 'text'
})
const data = res.data
const headers = res.headers
if (headers['content-disposition']) {
newItem.name = headers['content-disposition'].split('filename=')[1]
newItem.name = parseFilename(headers['content-disposition'])
}
if (headers['profile-web-page-url']) {
newItem.home = headers['profile-web-page-url']
}
if (headers['profile-update-interval']) {
newItem.interval = parseInt(headers['profile-update-interval']) * 60
}
if (headers['subscription-userinfo']) {
const extra = headers['subscription-userinfo']
.split(';')
.map((item: string) => item.split('=')[1].trim())
newItem.extra = {
upload: parseInt(extra[0]),
download: parseInt(extra[1]),
total: parseInt(extra[2]),
expire: parseInt(extra[3])
}
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
}
fs.writeFileSync(profilePath(id), data, 'utf-8')
} catch (e) {
@ -106,8 +137,9 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
if (force || !currentProfile) {
if (profileConfig.current) {
currentProfile = yaml.parse(fs.readFileSync(profilePath(profileConfig.current), 'utf-8'))
const current = getProfileConfig().current
if (current) {
currentProfile = yaml.parse(fs.readFileSync(profilePath(current), 'utf-8'))
} else {
currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8'))
}

View File

@ -1,12 +1,12 @@
import { ChildProcess, execSync, spawn } from 'child_process'
import { logPath, mihomoCorePath, mihomoWorkDir } from '../utils/dirs'
import { generateProfile } from '../resolve/factory'
import { appConfig } from '../config'
import { getAppConfig } from '../config'
import fs from 'fs'
let child: ChildProcess
export async function startCore(): Promise<void> {
const corePath = mihomoCorePath(appConfig.core ?? 'mihomo')
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
generateProfile()
stopCore()
if (process.platform !== 'win32') {

View File

@ -1,5 +1,5 @@
import axios, { AxiosInstance } from 'axios'
import { controledMihomoConfig } from '../config'
import { getControledMihomoConfig } from '../config'
import WebSocket from 'ws'
import { window } from '..'
@ -9,8 +9,8 @@ let mihomoTrafficWs: WebSocket = null!
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
if (axiosIns && !force) return axiosIns
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
axiosIns = axios.create({
@ -49,8 +49,8 @@ export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
}
export const mihomoTraffic = (): void => {
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?secret=${secret}`)

View File

@ -1,4 +1,9 @@
import { appConfig, controledMihomoConfig, setAppConfig, setControledMihomoConfig } from '../config'
import {
getAppConfig,
getControledMihomoConfig,
setAppConfig,
setControledMihomoConfig
} from '../config'
import icoIcon from '../../../resources/icon.ico?asset'
import pngIcon from '../../../resources/icon.png?asset'
import { patchMihomoConfig } from './mihomoApi'
@ -24,7 +29,7 @@ const buildContextMenu = (): Menu => {
id: 'rule',
label: '规则模式',
type: 'radio',
checked: controledMihomoConfig.mode === 'rule',
checked: getControledMihomoConfig().mode === 'rule',
click: (): void => {
setControledMihomoConfig({ mode: 'rule' })
patchMihomoConfig({ mode: 'rule' })
@ -36,7 +41,7 @@ const buildContextMenu = (): Menu => {
id: 'global',
label: '全局模式',
type: 'radio',
checked: controledMihomoConfig.mode === 'global',
checked: getControledMihomoConfig().mode === 'global',
click: (): void => {
setControledMihomoConfig({ mode: 'global' })
patchMihomoConfig({ mode: 'global' })
@ -48,7 +53,7 @@ const buildContextMenu = (): Menu => {
id: 'direct',
label: '直连模式',
type: 'radio',
checked: controledMihomoConfig.mode === 'direct',
checked: getControledMihomoConfig().mode === 'direct',
click: (): void => {
setControledMihomoConfig({ mode: 'direct' })
patchMihomoConfig({ mode: 'direct' })
@ -60,7 +65,7 @@ const buildContextMenu = (): Menu => {
{
type: 'checkbox',
label: '系统代理',
checked: appConfig.sysProxy?.enable ?? false,
checked: getAppConfig().sysProxy?.enable ?? false,
click: (item): void => {
const enable = item.checked
setAppConfig({ sysProxy: { enable } })
@ -72,7 +77,7 @@ const buildContextMenu = (): Menu => {
{
type: 'checkbox',
label: '虚拟网卡',
checked: controledMihomoConfig.tun?.enable ?? false,
checked: getControledMihomoConfig().tun?.enable ?? false,
click: (item): void => {
const enable = item.checked
setControledMihomoConfig({ tun: { enable } })

View File

@ -6,7 +6,7 @@ import icon from '../../resources/icon.png?asset'
import { mihomoTraffic } from './core/mihomoApi'
import { createTray } from './core/tray'
import { init } from './resolve/init'
import { appConfig } from './config'
import { getAppConfig } from './config'
import { join } from 'path'
export let window: BrowserWindow | null = null
@ -80,7 +80,7 @@ function createWindow(): void {
})
window.on('ready-to-show', () => {
if (!appConfig.silentStart) {
if (!getAppConfig().silentStart) {
window?.show()
window?.focusOnWebView()
}

View File

@ -1,9 +1,9 @@
import { controledMihomoConfig, currentProfile } from '../config'
import { getControledMihomoConfig, getCurrentProfile } from '../config'
import { mihomoWorkConfigPath } from '../utils/dirs'
import yaml from 'yaml'
import fs from 'fs'
export function generateProfile(): void {
const profile = Object.assign(currentProfile, controledMihomoConfig)
const profile = Object.assign(getCurrentProfile(), getControledMihomoConfig())
fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile))
}

View File

@ -9,12 +9,6 @@ import {
profilesDir,
resourcesFilesDir
} from '../utils/dirs'
import {
getAppConfig,
getControledMihomoConfig,
getCurrentProfile,
getProfileConfig
} from '../config'
import {
defaultConfig,
defaultControledMihomoConfig,
@ -53,16 +47,12 @@ function initConfig(): void {
if (!fs.existsSync(controledMihomoConfigPath())) {
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
}
getAppConfig(true)
getControledMihomoConfig(true)
getProfileConfig(true)
getCurrentProfile(true)
}
function initFiles(): void {
const fileList = ['Country.mmdb', 'geoip.dat', 'geosite.dat']
for (const file of fileList) {
const targetPath = path.join(profilesDir(), file)
const targetPath = path.join(mihomoWorkDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file)
if (!fs.existsSync(targetPath) && fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, targetPath)

View File

@ -1,4 +1,4 @@
import { controledMihomoConfig } from '../config'
import { getControledMihomoConfig } from '../config'
export function triggerSysProxy(enable: boolean): void {
if (enable) {
@ -9,7 +9,7 @@ export function triggerSysProxy(enable: boolean): void {
}
export function enableSysProxy(): void {
console.log('enableSysProxy', controledMihomoConfig['mixed-port'])
console.log('enableSysProxy', getControledMihomoConfig()['mixed-port'])
}
export function disableSysProxy(): void {

View File

@ -25,7 +25,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion)
ipcMain.handle('mihomoConfig', mihomoConfig)
ipcMain.handle('mihomoConnections', mihomoConnections)
ipcMain.handle('mihomeRules', mihomoRules)
ipcMain.handle('mihomoRules', mihomoRules)
ipcMain.handle('patchMihomoConfig', async (_e, patch) => await patchMihomoConfig(patch))
ipcMain.handle('checkAutoRun', checkAutoRun)
ipcMain.handle('enableAutoRun', enableAutoRun)

View File

@ -14,6 +14,7 @@ import OverrideCard from '@renderer/components/sider/override-card'
import ConnCard from '@renderer/components/sider/conn-card'
import LogCard from '@renderer/components/sider/log-card'
import MihomoCoreCard from './components/sider/mihomo-core-card.tsx'
import TestCard from './components/sider/test-card.js'
const App: React.FC = () => {
const { setTheme } = useTheme()
@ -74,14 +75,15 @@ const App: React.FC = () => {
<ProfileCard />
<ProxyCard />
<MihomoCoreCard />
<ConnCard />
</div>
<div className="flex justify-between mx-2">
<ConnCard />
<LogCard />
<RuleCard />
</div>
<div className="flex justify-between mx-2">
<RuleCard />
<TestCard />
<OverrideCard />
</div>
{/* </div> */}

View File

@ -1,22 +1,22 @@
import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react'
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import { useEffect, useState } from 'react'
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 match = location.pathname.includes('/connections')
const { data: connections } = useSWR<IMihomoConnectionsInfo>('/connections', mihomoConnections, {
refreshInterval: 5000
})
const [upload, setUpload] = useState(0)
const [download, setDownload] = useState(0)
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => {
console.log(info)
setUpload(info.up)
setDownload(info.down)
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
@ -25,7 +25,8 @@ const ConnCard: React.FC = () => {
return (
<Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
fullWidth
className={`mb-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/connections')}
>
@ -39,24 +40,16 @@ const ConnCard: React.FC = () => {
>
<IoLink color="default" className="text-[20px]" />
</Button>
<Chip
classNames={
match
? {
base: 'border-foreground',
content: 'text-foreground'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{connections?.connections?.length ?? 0}
</Chip>
<div className="p-2 w-full">
<div className="flex justify-between">
<div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div>
<FaCircleArrowUp className="h-[24px] leading-[24px]" />
</div>
<div className="flex justify-between">
<div className="w-full text-right mr-2">{calcTraffic(download)}/s</div>
<FaCircleArrowDown className="h-[24px] leading-[24px]" />
</div>
</div>
</div>
</CardBody>
<CardFooter className="pt-1">

View File

@ -8,7 +8,7 @@ const LogCard: React.FC = () => {
const match = location.pathname.includes('/logs')
return (
<Card
className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`}
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/logs')}
>

View File

@ -1,4 +1,4 @@
import { Button, Card, CardBody, CardFooter, cn } from '@nextui-org/react'
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React, { useState } from 'react'
import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom'

View File

@ -1,29 +1,25 @@
import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
import { getCurrentProfileItem } from '@renderer/utils/ipc'
import { useEffect } from 'react'
import { IoMdRefresh } from 'react-icons/io'
import { useProfileConfig } from '@renderer/hooks/use-profile'
import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import useSWR from 'swr'
import { IoMdRefresh } from 'react-icons/io'
const ProfileCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/profiles')
const { data: info, mutate } = useSWR('getCurrentProfileItem', getCurrentProfileItem)
const { profileConfig } = useProfileConfig()
const { current, items } = profileConfig ?? {}
const info = items?.find((item) => item.id === current) ?? {
id: 'default',
type: 'local',
name: '空白订阅'
}
const extra = info?.extra
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
const total = extra?.total ?? 0
useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutate()
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
}
})
return (
<Card
fullWidth
@ -43,8 +39,8 @@ const ProfileCard: React.FC = () => {
</CardBody>
<CardFooter className="pt-1">
<Progress
classNames={{ indicator: 'bg-foreground' }}
label={`${calcTraffic(usage)}/${calcTraffic(total)}`}
classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
label={extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
className="max-w-md"
/>

View File

@ -8,13 +8,13 @@ const RuleCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/rules')
const { data: rules } = useSWR<IMihomoRulesInfo>('/connections', mihomoRules, {
const { data: rules } = useSWR<IMihomoRulesInfo>('/rules', mihomoRules, {
refreshInterval: 5000
})
return (
<Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/rules')}
>

View File

@ -0,0 +1,36 @@
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React from 'react'
import { TbWorldCheck } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom'
const TestCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/tests')
return (
<Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/tests')}
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<TbWorldCheck color="default" className="text-[20px]" />
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className="select-none text-md font-bold"></h3>
</CardFooter>
</Card>
)
}
export default TestCard

View File

@ -21,15 +21,12 @@ export const useProfileConfig = (): RetuenType => {
const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => {
await add(item)
mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
}
const removeProfileItem = async (id: string): Promise<void> => {
await remove(id)
mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
}
useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutateProfileConfig()

View File

@ -0,0 +1,5 @@
const Tests: React.FC = () => {
return <div>Tests</div>
}
export default Tests

View File

@ -9,6 +9,7 @@ import Connections from '@renderer/pages/connections'
import Mihomo from '@renderer/pages/mihomo'
import Sysproxy from '@renderer/pages/syspeoxy'
import Tun from '@renderer/pages/tun'
import Tests from '@renderer/pages/tests'
const routes = [
{
@ -31,6 +32,10 @@ const routes = [
path: '/rules',
element: <Rules />
},
{
path: '/tests',
element: <Tests />
},
{
path: '/logs',
element: <Logs />

View File

@ -1,6 +1,19 @@
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`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} KB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} MB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} GB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} TB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} PB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} EB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} ZB`
bit /= 1024
return `${bit.toFixed(2)} YB`
}

17
src/shared/types.d.ts vendored
View File

@ -74,6 +74,7 @@ interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha'
silentStart: boolean
sysProxy: ISysProxyConfig
userAgent?: string
}
interface IMihomoTunConfig {
@ -127,17 +128,21 @@ interface IProfileConfig {
items: IProfileItem[]
}
interface ISubscriptionUserInfo {
upload: number
download: number
total: number
expire: number
}
interface IProfileItem {
id: string
type: 'remote' | 'local'
name: string
url?: string // remote
file?: string // local
interval?: number
home?: string
updated?: number
extra?: {
upload: number
download: number
total: number
expire: number
}
extra?: ISubscriptionUserInfo
}