setup core manager

This commit is contained in:
pompurin404 2024-08-01 08:33:01 +08:00
parent f30d1228f1
commit 574bdb05be
No known key found for this signature in database
16 changed files with 281 additions and 25 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -4,9 +4,11 @@ import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun'
import { import {
getAppConfig, getAppConfig,
setAppConfig, setAppConfig,
getProfileConfig,
getControledMihomoConfig, getControledMihomoConfig,
setControledMihomoConfig setControledMihomoConfig
} from './config' } from './config'
import { restartCore } from './manager'
export function registerIpcMainHandlers(): void { export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoVersion', mihomoVersion)
@ -21,4 +23,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('setControledMihomoConfig', (_e, config) => { ipcMain.handle('setControledMihomoConfig', (_e, config) => {
setControledMihomoConfig(config) setControledMihomoConfig(config)
}) })
ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force))
ipcMain.handle('restartCore', () => restartCore())
} }

View File

@ -1,50 +1,80 @@
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import fs from 'fs'
import { app } from 'electron' import {
import path from 'path' defaultConfig,
import { defaultConfig } from './template' defaultControledMihomoConfig,
defaultProfile,
const dataDir = app.getPath('userData') defaultProfileConfig
const appConfigPath = path.join(dataDir, 'config.yaml') } from './template'
const controledMihomoConfigPath = path.join(dataDir, 'mihomo.yaml') import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs'
export let appConfig: IAppConfig export let appConfig: IAppConfig
export let profileConfig: IProfileConfig
export let currentProfile: Partial<IMihomoConfig>
export let controledMihomoConfig: Partial<IMihomoConfig> export let controledMihomoConfig: Partial<IMihomoConfig>
export function initConfig(): void { export function initConfig(): void {
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(appConfigPath())) {
fs.mkdirSync(dataDir) fs.writeFileSync(appConfigPath(), yaml.stringify(defaultConfig))
} }
if (!fs.existsSync(appConfigPath)) { if (!fs.existsSync(profileConfigPath())) {
fs.writeFileSync(appConfigPath, yaml.stringify(defaultConfig)) fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig))
} }
if (!fs.existsSync(controledMihomoConfigPath)) { if (!fs.existsSync(profilePath('default'))) {
fs.writeFileSync(controledMihomoConfigPath, yaml.stringify({})) fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile))
}
if (!fs.existsSync(controledMihomoConfigPath())) {
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
} }
getAppConfig(true) getAppConfig(true)
getControledMihomoConfig(true) getControledMihomoConfig(true)
getProfileConfig(true)
getCurrentProfile(true)
} }
export function getAppConfig(force = false): IAppConfig { export function getAppConfig(force = false): IAppConfig {
if (force || !appConfig) { if (force || !appConfig) {
appConfig = yaml.parse(fs.readFileSync(appConfigPath, 'utf-8')) appConfig = yaml.parse(fs.readFileSync(appConfigPath(), 'utf-8'))
} }
return appConfig return appConfig
} }
export function setAppConfig(patch: Partial<IAppConfig>): void { export function setAppConfig(patch: Partial<IAppConfig>): void {
appConfig = Object.assign(appConfig, patch) appConfig = Object.assign(appConfig, patch)
fs.writeFileSync(appConfigPath, yaml.stringify(appConfig)) fs.writeFileSync(appConfigPath(), yaml.stringify(appConfig))
} }
export function getControledMihomoConfig(force = false): Partial<IMihomoConfig> { export function getControledMihomoConfig(force = false): Partial<IMihomoConfig> {
if (force || !controledMihomoConfig) { if (force || !controledMihomoConfig) {
controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath, 'utf-8')) controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath(), 'utf-8'))
} }
return controledMihomoConfig return controledMihomoConfig
} }
export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void { export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void {
controledMihomoConfig = Object.assign(controledMihomoConfig, patch) controledMihomoConfig = Object.assign(controledMihomoConfig, patch)
fs.writeFileSync(controledMihomoConfigPath, yaml.stringify(controledMihomoConfig)) fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig))
}
export function getProfileConfig(force = false): IProfileConfig {
if (force || !profileConfig) {
profileConfig = yaml.parse(fs.readFileSync(profileConfigPath(), 'utf-8'))
}
return profileConfig
}
export function setProfileConfig(patch: Partial<IProfileConfig>): void {
profileConfig = Object.assign(profileConfig, patch)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
}
export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
if (force || !currentProfile) {
if (profileConfig.current) {
currentProfile = yaml.parse(fs.readFileSync(profilePath(profileConfig.current), 'utf-8'))
} else {
currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8'))
}
}
return currentProfile
} }

59
src/main/dirs.ts Normal file
View File

@ -0,0 +1,59 @@
import { is } from '@electron-toolkit/utils'
import { app } from 'electron'
import path from 'path'
import fs from 'fs'
export const dataDir = app.getPath('userData')
export function initDirs(): void {
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir)
}
if (!fs.existsSync(profilesDir())) {
fs.mkdirSync(profilesDir())
}
if (!fs.existsSync(mihomoWorkDir())) {
fs.mkdirSync(mihomoWorkDir())
}
}
export function mihomoCoreDir(): string {
if (is.dev) {
return path.join(__dirname, '../../resources/sidecar')
} else {
return path.join(process.resourcesPath, 'sidecar')
}
}
export function mihomoCorePath(core: string): string {
const isWin = process.platform === 'win32'
return path.join(mihomoCoreDir(), `${core}${isWin ? '.exe' : ''}`)
}
export function appConfigPath(): string {
return path.join(dataDir, 'config.yaml')
}
export function controledMihomoConfigPath(): string {
return path.join(dataDir, 'mihomo.yaml')
}
export function profileConfigPath(): string {
return path.join(dataDir, 'profile.yaml')
}
export function profilesDir(): string {
return path.join(dataDir, 'profiles')
}
export function profilePath(id: string): string {
return path.join(profilesDir(), `${id}.yaml`)
}
export function mihomoWorkDir(): string {
return path.join(dataDir, 'work')
}
export function mihomoWorkConfigPath(): string {
return path.join(mihomoWorkDir(), 'config.yaml')
}

9
src/main/factory.ts Normal file
View File

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

View File

@ -5,13 +5,16 @@ import pngIcon from '../../resources/icon.png?asset'
import icoIcon from '../../resources/icon.ico?asset' import icoIcon from '../../resources/icon.ico?asset'
import { registerIpcMainHandlers } from './cmds' import { registerIpcMainHandlers } from './cmds'
import { initConfig, appConfig } from './config' import { initConfig, appConfig } from './config'
import { stopCore, startCore } from './manager'
import { initDirs } from './dirs'
let window: BrowserWindow | null = null let window: BrowserWindow | null = null
let tray: Tray | null = null let tray: Tray | null = null
let trayContextMenu: Menu | null = null let trayContextMenu: Menu | null = null
initDirs()
initConfig() initConfig()
startCore()
function createWindow(): void { function createWindow(): void {
// Create the browser window. // Create the browser window.
window = new BrowserWindow({ window = new BrowserWindow({
@ -124,5 +127,6 @@ app.on('window-all-closed', () => {
}) })
app.on('before-quit', () => { app.on('before-quit', () => {
stopCore()
app.exit() app.exit()
}) })

25
src/main/manager.ts Normal file
View File

@ -0,0 +1,25 @@
import { execFile, ChildProcess } from 'child_process'
import { mihomoCorePath, mihomoWorkDir } from './dirs'
import { generateProfile } from './factory'
import { appConfig } from './config'
let child: ChildProcess
export function startCore(): void {
const corePath = mihomoCorePath(appConfig.core ?? 'mihomo')
generateProfile()
stopCore()
child = execFile(corePath, ['-d', mihomoWorkDir()], (error, stdout) => {
console.log(stdout)
})
}
export function stopCore(): void {
if (child) {
child.kill('SIGINT')
}
}
export function restartCore(): void {
startCore()
}

View File

@ -1,4 +1,5 @@
import axios, { AxiosInstance } from 'axios' import axios, { AxiosInstance } from 'axios'
import { controledMihomoConfig } from './config'
let axiosIns: AxiosInstance = null! let axiosIns: AxiosInstance = null!
@ -7,8 +8,9 @@ let axiosIns: AxiosInstance = null!
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => { export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
if (axiosIns && !force) return axiosIns if (axiosIns && !force) return axiosIns
const server = '127.0.0.1:9097' let server = controledMihomoConfig['external-controller']
const secret = '' const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
axiosIns = axios.create({ axiosIns = axios.create({
baseURL: `http://${server}`, baseURL: `http://${server}`,

View File

@ -1,3 +1,30 @@
export const defaultConfig: IAppConfig = { export const defaultConfig: IAppConfig = {
core: 'mihomo',
silentStart: false silentStart: false
} }
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
'external-controller': '127.0.0.1:9090',
ipv6: false,
mode: 'rule',
'mixed-port': 7890,
'allow-lan': false,
'log-level': 'info'
}
export const defaultProfileConfig: IProfileConfig = {
current: 'default',
profiles: [
{
id: 'default',
type: 'local',
name: '默认'
}
]
}
export const defaultProfile: Partial<IMihomoConfig> = {
proxies: [],
'proxy-groups': [],
rules: []
}

View File

@ -13,6 +13,7 @@ import RuleCard from '@renderer/components/sider/rule-card'
import OverrideCard from '@renderer/components/sider/override-card' import OverrideCard from '@renderer/components/sider/override-card'
import ConnCard from '@renderer/components/sider/conn-card' import ConnCard from '@renderer/components/sider/conn-card'
import LogCard from '@renderer/components/sider/log-card' import LogCard from '@renderer/components/sider/log-card'
import MihomoCoreCard from './components/sider/mihomo-core-card.tsx'
const App: React.FC = () => { const App: React.FC = () => {
const { setTheme } = useTheme() const { setTheme } = useTheme()
@ -67,6 +68,7 @@ const App: React.FC = () => {
<h3 className="select-none text-lg font-bold m-2"></h3> <h3 className="select-none text-lg font-bold m-2"></h3>
<div className="w-full h-[calc(100%-260px)] overflow-y-auto no-scrollbar"> <div className="w-full h-[calc(100%-260px)] overflow-y-auto no-scrollbar">
<div className="mx-2"> <div className="mx-2">
<MihomoCoreCard />
<ProfileCard /> <ProfileCard />
<ProxyCard /> <ProxyCard />
</div> </div>

View File

@ -1,4 +1,4 @@
import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
@ -22,9 +22,6 @@ const ConnCard: React.FC = () => {
> >
<IoLink color="default" className="text-[20px]" /> <IoLink color="default" className="text-[20px]" />
</Button> </Button>
<Chip size="sm" color="secondary" variant="bordered" className="mr-3 mt-2">
1103
</Chip>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">

View File

@ -0,0 +1,72 @@
import {
Button,
Card,
CardBody,
CardFooter,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger
} from '@nextui-org/react'
import { useAppConfig } from '@renderer/hooks/use-config'
import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
import { IoMdRefresh } from 'react-icons/io'
import useSWR from 'swr'
const CoreMap = {
mihomo: 'Mihomo',
'mihomo-alpha': 'Mihomo Alpha'
}
const MihomoCoreCard: React.FC = () => {
const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion)
const { appConfig, patchAppConfig } = useAppConfig()
const { core } = appConfig || {}
return (
<Card
fullWidth
className={`mb-2 ${location.pathname.includes('/profiles') ? 'bg-primary' : ''}`}
>
<CardBody>
<div className="flex justify-between h-[32px]">
<h3 className="select-none text-md font-bold leading-[32px]">
{version?.version ?? '-'}
</h3>
<Button
isIconOnly
size="sm"
variant="light"
color="default"
onPress={() => {
restartCore()
}}
>
<IoMdRefresh color="default" className="text-[24px]" />
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<Dropdown>
<DropdownTrigger>
<Button variant="faded" fullWidth>
{core ? CoreMap[core] : ''}
</Button>
</DropdownTrigger>
<DropdownMenu
onAction={async (key) => {
await patchAppConfig({ core: key as 'mihomo' | 'mihomo-alpha' })
await restartCore()
await mutate()
}}
>
<DropdownItem key="mihomo">Mihomo </DropdownItem>
<DropdownItem key="mihomo-alpha">Mihomo Alpha</DropdownItem>
</DropdownMenu>
</Dropdown>
</CardFooter>
</Card>
)
}
export default MihomoCoreCard

View File

@ -4,7 +4,7 @@ import { getAppConfig, setAppConfig } from '@renderer/utils/ipc'
interface RetuenType { interface RetuenType {
appConfig: IAppConfig | undefined appConfig: IAppConfig | undefined
mutateAppConfig: () => void mutateAppConfig: () => void
patchAppConfig: (value: Partial<IAppConfig>) => void patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
} }
export const useAppConfig = (): RetuenType => { export const useAppConfig = (): RetuenType => {

View File

@ -29,3 +29,7 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
export async function setControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> { export async function setControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch)
} }
export async function restartCore(): Promise<void> {
await window.electron.ipcRenderer.invoke('restartCore')
}

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

@ -1,4 +1,5 @@
type OutboundMode = 'rule' | 'global' | 'direct' type OutboundMode = 'rule' | 'global' | 'direct'
type LogLevel = 'info' | 'debug' | 'warn' | 'error' | 'silent'
interface IMihomoVersion { interface IMihomoVersion {
version: string version: string
@ -6,12 +7,32 @@ interface IMihomoVersion {
} }
interface IAppConfig { interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha'
silentStart: boolean silentStart: boolean
} }
interface IMihomoConfig { interface IMihomoConfig {
'external-controller': string
secret?: string
ipv6: boolean
mode: OutboundMode mode: OutboundMode
'mixed-port': number 'mixed-port': number
'allow-lan': boolean
'log-level': LogLevel
'socks-port'?: number 'socks-port'?: number
port?: number port?: number
proxies?: []
'proxy-groups'?: []
rules?: []
}
interface IProfileConfig {
current?: string
profiles?: IProfileItem[]
}
interface IProfileItem {
id: string
type: 'remote' | 'local'
name: string
} }