feat: replace restartCore with hot reload for config switches and overrides

This commit is contained in:
zjdndjf
2026-04-17 13:24:36 +08:00
parent 68195d122b
commit 00e551d728
27 changed files with 119 additions and 73 deletions

View File

@@ -1,6 +1,5 @@
import { readFile, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { ipcMain } from 'electron'
import { controledMihomoConfigPath } from '../utils/dirs'
import { parse, stringify } from '../utils/yaml'
import { generateProfile } from '../core/factory'
@@ -67,8 +66,6 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
) {
// 恢复 DNS 状态并清除保存的状态
await patchAppConfig({ controlDns: controlDnsBeforePause, controlDnsBeforePause: undefined })
// 通过事件通知重启核心,避免循环依赖
ipcMain.emit('restartCore')
}
// 过滤端口字段中的 NaN 值,防止写入无效配置
@@ -102,6 +99,17 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
await generateProfile()
await writeFile(controledMihomoConfigPath(), stringify(controledMihomoConfig), 'utf-8')
// 优先对运行中内核进行热更新,避免无意义重启
try {
const { patchMihomoConfig } = await import('../core/mihomoApi')
await patchMihomoConfig(patch)
} catch (error) {
controledMihomoLogger.warn(
'Hot patch /configs failed, changes will apply on next restart',
error
)
}
})
await controledMihomoWriteQueue
}

View File

@@ -7,7 +7,7 @@ import * as chromeRequest from '../utils/chromeRequest'
import { parse, stringify } from '../utils/yaml'
import { defaultProfile } from '../utils/template'
import { subStorePort } from '../resolve/server'
import { mihomoUpgradeConfig } from '../core/mihomoApi'
import { mihomoUpgradeConfig, mihomoHotReloadConfig } from '../core/mihomoApi'
import { restartCore } from '../core/manager'
import { addProfileUpdater, removeProfileUpdater } from '../core/profileUpdater'
import { mihomoProfileWorkDir, mihomoWorkDir, profileConfigPath, profilePath } from '../utils/dirs'
@@ -77,7 +77,12 @@ export async function changeCurrentProfile(id: string): Promise<void> {
config.current = id
return config
})
await restartCore()
const { useHotReloadProfile = false } = await getAppConfig()
if (useHotReloadProfile) {
await mihomoHotReloadConfig()
} else {
await restartCore()
}
} catch (e) {
// 回滚配置
await updateProfileConfig((config) => {

View File

@@ -224,6 +224,19 @@ export const mihomoUpgradeConfig = async (): Promise<void> => {
}
}
export const mihomoHotReloadConfig = async (): Promise<void> => {
mihomoApiLogger.info('mihomoHotReloadConfig called')
const { generateProfile } = await import('./factory')
const current = await generateProfile()
const { diffWorkDir = false } = await getAppConfig()
const { mihomoWorkConfigPath } = await import('../utils/dirs')
const configPath = diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work')
mihomoApiLogger.info(`hot reload config path: ${configPath}`)
const instance = await getAxios()
await instance.put('/configs?force=true', { path: configPath })
mihomoApiLogger.info('hot reload config completed')
}
// Smart 内核 API
export const mihomoSmartGroupWeights = async (
groupName: string

View File

@@ -7,8 +7,7 @@ import {
patchControledMihomoConfig
} from '../config'
import { triggerSysProxy } from '../sys/sysproxy'
import { patchMihomoConfig } from '../core/mihomoApi'
import { quitWithoutCore, restartCore } from '../core/manager'
import { quitWithoutCore } from '../core/manager'
import i18next from '../../shared/i18n'
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
import { copyEnv, updateTrayIcon } from './tray'
@@ -70,7 +69,6 @@ export async function registerShortcut(
} else {
await patchControledMihomoConfig({ tun: { enable: !enable } })
}
await restartCore()
new Notification({
title: i18next.t(
!enable ? 'common.notification.tunEnabled' : 'common.notification.tunDisabled'
@@ -89,7 +87,6 @@ export async function registerShortcut(
case 'ruleModeShortcut': {
return globalShortcut.register(newShortcut, async () => {
await patchControledMihomoConfig({ mode: 'rule' })
await patchMihomoConfig({ mode: 'rule' })
new Notification({
title: i18next.t('common.notification.ruleMode')
}).show()
@@ -101,7 +98,6 @@ export async function registerShortcut(
case 'globalModeShortcut': {
return globalShortcut.register(newShortcut, async () => {
await patchControledMihomoConfig({ mode: 'global' })
await patchMihomoConfig({ mode: 'global' })
new Notification({
title: i18next.t('common.notification.globalMode')
}).show()
@@ -113,7 +109,6 @@ export async function registerShortcut(
case 'directModeShortcut': {
return globalShortcut.register(newShortcut, async () => {
await patchControledMihomoConfig({ mode: 'direct' })
await patchMihomoConfig({ mode: 'direct' })
new Notification({
title: i18next.t('common.notification.directMode')
}).show()

View File

@@ -30,7 +30,6 @@ import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy'
import {
quitWithoutCore,
restartCore,
checkMihomoCorePermissions,
requestTunPermissions,
restartAsAdmin
@@ -264,7 +263,6 @@ export const buildContextMenu = async (): Promise<Menu> => {
}
mainWindow?.webContents.send('controledMihomoConfigUpdated')
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
await restartCore()
} catch {
// ignore
} finally {

View File

@@ -21,6 +21,7 @@ import {
mihomoUpgradeGeo,
mihomoUpgradeUI,
mihomoUpgradeConfig,
mihomoHotReloadConfig,
mihomoVersion,
patchMihomoConfig,
mihomoSmartGroupWeights,
@@ -284,6 +285,7 @@ const asyncHandlers: Record<string, AsyncFn> = {
readTextFile,
// Core
restartCore,
mihomoHotReloadConfig,
startMonitor,
quitWithoutCore,
// System

View File

@@ -59,7 +59,8 @@ export const defaultConfig: IAppConfig = {
enableRedirPort: false,
showTproxyPort: 0,
enableTproxyPort: false,
testProfileOnStart: true
testProfileOnStart: true,
useHotReloadProfile: false
}
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {

View File

@@ -13,9 +13,9 @@ const SettingItem: React.FC<Props> = (props) => {
return (
<>
<div className="select-text h-[32px] w-full flex justify-between">
<div className="select-text h-8 w-full flex justify-between">
<div className="h-full flex items-center">
<h4 className="h-full text-md leading-[32px] whitespace-nowrap">{title}</h4>
<h4 className="h-full text-md leading-8 whitespace-nowrap">{title}</h4>
<div>{actions}</div>
</div>
{children}

View File

@@ -1,7 +1,7 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import React, { useEffect, useState } from 'react'
import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
import { getOverride, mihomoHotReloadConfig, setOverride } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
import { BaseEditor } from '../base/base-editor'
@@ -58,7 +58,7 @@ const EditFileModal: React.FC<Props> = (props) => {
onPress={async () => {
try {
await setOverride(id, language === 'javascript' ? 'js' : 'yaml', currData)
await restartCore()
await mihomoHotReloadConfig()
onClose()
} catch (e) {
toast.error(String(e))

View File

@@ -9,7 +9,7 @@ import {
Switch
} from '@heroui/react'
import React, { useState } from 'react'
import { restartCore } from '@renderer/utils/ipc'
import { mihomoHotReloadConfig } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
import SettingItem from '../base/base-setting-item'
@@ -25,7 +25,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
const onSave = async (): Promise<void> => {
await updateOverrideItem(values)
await restartCore()
await mihomoHotReloadConfig()
onClose()
}

View File

@@ -14,7 +14,7 @@ import dayjs from '@renderer/utils/dayjs'
import React, { Key, useMemo, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { openFile, restartCore } from '@renderer/utils/ipc'
import { openFile, mihomoHotReloadConfig } from '@renderer/utils/ipc'
import { useTranslation } from 'react-i18next'
import ExecLogModal from './exec-log-modal'
import EditInfoModal from './edit-info-modal'
@@ -189,7 +189,7 @@ const OverrideItem: React.FC<Props> = (props) => {
setUpdating(true)
try {
await addOverrideItem(info)
await restartCore()
await mihomoHotReloadConfig()
} catch (e) {
toast.error(String(e))
} finally {

View File

@@ -16,7 +16,7 @@ import {
import { toast } from '@renderer/components/base/toast'
import React, { useState } from 'react'
import { useOverrideConfig } from '@renderer/hooks/use-override-config'
import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
import { mihomoHotReloadConfig, addProfileUpdater } from '@renderer/utils/ipc'
import { MdDeleteForever } from 'react-icons/md'
import { FaPlus } from 'react-icons/fa6'
import { useTranslation } from 'react-i18next'
@@ -49,7 +49,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
}
await updateProfileItem(updatedItem)
await addProfileUpdater(updatedItem)
await restartCore()
await mihomoHotReloadConfig()
onClose()
} catch (e) {
toast.error(String(e))

View File

@@ -17,6 +17,7 @@ const MihomoConfig: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig()
const {
diffWorkDir = false,
useHotReloadProfile = false,
delayTestConcurrency,
delayTestTimeout,
githubToken = '',
@@ -57,7 +58,7 @@ const MihomoConfig: React.FC = () => {
<div className="flex items-center gap-2">
<Input
size="sm"
className="w-[100px]"
className="w-25"
type="number"
value={(subscriptionTimeout / 1000)?.toString()}
onValueChange={async (v: string) => {
@@ -148,7 +149,7 @@ const MihomoConfig: React.FC = () => {
<SettingItem title={t('mihomo.proxyColumns.title')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
className="w-37.5"
size="sm"
selectedKeys={new Set([proxyCols])}
aria-label={t('mihomo.proxyColumns.title')}
@@ -168,7 +169,7 @@ const MihomoConfig: React.FC = () => {
<SettingItem title={t('mihomo.cpuPriority.title')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
className="w-37.5"
size="sm"
selectedKeys={new Set([mihomoCpuPriority])}
disallowEmptySelection={true}
@@ -221,6 +222,26 @@ const MihomoConfig: React.FC = () => {
/>
</SettingItem>
<SettingItem
title={t('mihomo.hotReloadProfile.title')}
actions={
<Tooltip content={t('mihomo.hotReloadProfile.tooltip')}>
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
</Tooltip>
}
divider
>
<Switch
size="sm"
isSelected={useHotReloadProfile}
onValueChange={(v) => {
patchAppConfig({ useHotReloadProfile: v })
}}
/>
</SettingItem>
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
<Switch
size="sm"

View File

@@ -1,10 +1,9 @@
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import { toast } from '@renderer/components/base/toast'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import BorderSwitch from '@renderer/components/base/border-swtich'
import { LuServer } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore } from '@renderer/utils/ipc'
import { mihomoHotReloadConfig } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
@@ -26,7 +25,6 @@ const DNSCard: React.FC<Props> = (props) => {
const location = useLocation()
const navigate = useNavigate()
const match = location.pathname.includes('/dns')
const { patchControledMihomoConfig } = useControledMihomoConfig()
const {
attributes,
listeners,
@@ -41,8 +39,7 @@ const DNSCard: React.FC<Props> = (props) => {
const onChange = async (controlDns: boolean): Promise<void> => {
try {
await patchAppConfig({ controlDns })
await patchControledMihomoConfig({})
await restartCore()
await mihomoHotReloadConfig()
} catch (e) {
toast.error(String(e))
}

View File

@@ -3,8 +3,7 @@ import { toast } from '@renderer/components/base/toast'
import BorderSwitch from '@renderer/components/base/border-swtich'
import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore } from '@renderer/utils/ipc'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { mihomoHotReloadConfig } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
@@ -26,7 +25,6 @@ const SniffCard: React.FC<Props> = (props) => {
const location = useLocation()
const navigate = useNavigate()
const match = location.pathname.includes('/sniffer')
const { patchControledMihomoConfig } = useControledMihomoConfig()
const {
attributes,
listeners,
@@ -41,8 +39,7 @@ const SniffCard: React.FC<Props> = (props) => {
const onChange = async (controlSniff: boolean): Promise<void> => {
try {
await patchAppConfig({ controlSniff })
await patchControledMihomoConfig({})
await restartCore()
await mihomoHotReloadConfig()
} catch (e) {
toast.error(String(e))
}

View File

@@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import BorderSwitch from '@renderer/components/base/border-swtich'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore, updateTrayIconImmediate } from '@renderer/utils/ipc'
import { updateTrayIconImmediate } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import React from 'react'
@@ -96,7 +96,6 @@ const TunSwitcher: React.FC<Props> = (props) => {
} else {
await patchControledMihomoConfig({ tun: { enable } })
}
await restartCore()
window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu')
}

View File

@@ -161,6 +161,8 @@
"mihomo.cpuPriority.low": "Low",
"mihomo.workDir.title": "Separate Work Directory for Different Subscriptions",
"mihomo.workDir.tooltip": "Enable to avoid conflicts when different subscriptions have proxy groups with the same name",
"mihomo.hotReloadProfile.title": "Hot Reload Config on Profile Switch",
"mihomo.hotReloadProfile.tooltip": "When enabled, switching profiles uses the API to hot reload config without restarting the core. When disabled, the core restarts as usual.",
"mihomo.controlSniff": "Control Domain Sniffing",
"mihomo.autoCloseConnection": "Auto Close Connection",
"mihomo.testProfileOnStart": "Test Profile on Start",

View File

@@ -141,6 +141,8 @@
"mihomo.cpuPriority.low": "پایین",
"mihomo.workDir.title": "استفاده از پوشه کاری مجزا برای اشتراک‌های مختلف",
"mihomo.workDir.tooltip": "برای جلوگیری از تداخل گروه‌های پراکسی با نام یکسان در اشتراک‌های مختلف",
"mihomo.hotReloadProfile.title": "بارگذاری گرم پیکربندی در تغییر پروفایل",
"mihomo.hotReloadProfile.tooltip": "در صورت فعال بودن، تغییر پروفایل بدون راه‌اندازی مجدد هسته انجام می‌شود",
"mihomo.controlSniff": "کنترل تشخیص دامنه",
"mihomo.autoCloseConnection": "بستن خودکار اتصال",
"mihomo.testProfileOnStart": "بررسی پروفایل در شروع",

View File

@@ -143,6 +143,8 @@
"mihomo.cpuPriority.low": "Низкий",
"mihomo.workDir.title": "Отдельные рабочие каталоги для разных подписок",
"mihomo.workDir.tooltip": "Включите для избежания конфликтов при наличии групп прокси с одинаковыми именами в разных подписках",
"mihomo.hotReloadProfile.title": "Горячая перезагрузка конфигурации при смене профиля",
"mihomo.hotReloadProfile.tooltip": "При включении смена профиля выполняется через API без перезапуска ядра; при отключении используется перезапуск ядра",
"mihomo.controlSniff": "Управление сниффингом доменов",
"mihomo.autoCloseConnection": "Автозакрытие соединений",
"mihomo.testProfileOnStart": "Проверять профиль при запуске",

View File

@@ -164,6 +164,8 @@
"mihomo.cpuPriority.low": "低",
"mihomo.workDir.title": "不同订阅使用独立工作目录",
"mihomo.workDir.tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突",
"mihomo.hotReloadProfile.title": "切换订阅时热重载配置",
"mihomo.hotReloadProfile.tooltip": "启用后切换订阅时通过 API 热重载配置而无需重启内核,禁用时仍使用重启方式切换",
"mihomo.controlSniff": "控制域名嗅探",
"mihomo.autoCloseConnection": "自动关闭连接",
"mihomo.testProfileOnStart": "启动时检查配置文件",

View File

@@ -164,6 +164,8 @@
"mihomo.cpuPriority.low": "低",
"mihomo.workDir.title": "不同訂閱使用獨立工作目錄",
"mihomo.workDir.tooltip": "啟用後可避免不同訂閱中存在相同名稱的代理組時發生衝突",
"mihomo.hotReloadProfile.title": "切換訂閱時熱重載配置",
"mihomo.hotReloadProfile.tooltip": "啟用後切換訂閱時透過 API 熱重載配置而無需重啟內核,禁用時仍使用重啟方式切換",
"mihomo.controlSniff": "控制域名嗅探",
"mihomo.autoCloseConnection": "自動關閉連接",
"mihomo.testProfileOnStart": "啟動時檢查配置檔案",

View File

@@ -6,7 +6,7 @@ import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { restartCore, patchMihomoConfig } from '@renderer/utils/ipc'
import { mihomoHotReloadConfig } from '@renderer/utils/ipc'
import React, { Key, ReactNode, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -144,8 +144,7 @@ const DNS: React.FC = () => {
setChanged(false)
await patchControledMihomoConfig(patch)
if (controlDns) {
await patchMihomoConfig(patch)
await restartCore()
await mihomoHotReloadConfig()
}
} catch (e) {
showErrorSync(e, t('common.error.dnsConfigSaveFailed'))
@@ -427,7 +426,7 @@ const DNS: React.FC = () => {
<SettingItem title={t('dns.fallbackFilter.geoipCode')} divider>
<Input
size="sm"
className="w-[100px]"
className="w-25"
value={typeof values.fallbackGeoipCode === 'string' ? values.fallbackGeoipCode : ''}
placeholder="CN"
onValueChange={(v) => {

View File

@@ -36,6 +36,7 @@ import {
import PubSub from 'pubsub-js'
import {
mihomoUpgrade,
mihomoHotReloadConfig,
restartCore,
startSubStoreBackendServer,
triggerSysProxy,
@@ -319,7 +320,6 @@ const Mihomo: React.FC = () => {
const onChangeNeedRestart = async (patch: Partial<IMihomoConfig>): Promise<void> => {
await patchControledMihomoConfig(patch)
await restartCore()
}
const handleConfigChangeWithRestart = async (key: string, value: unknown) => {
@@ -472,7 +472,7 @@ const Mihomo: React.FC = () => {
color="primary"
onValueChange={async (v) => {
await patchAppConfig({ enableSmartOverride: v })
await restartCore()
await mihomoHotReloadConfig()
}}
/>
</SettingItem>
@@ -535,7 +535,7 @@ const Mihomo: React.FC = () => {
? 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50'
: 'data-[hover=true]:bg-default-200'
}}
className="w-[150px]"
className="w-37.5"
size="sm"
aria-label={t('mihomo.selectCoreVersion')}
selectedKeys={new Set([core])}
@@ -587,7 +587,7 @@ const Mihomo: React.FC = () => {
isSelected={smartCoreUseLightGBM}
onValueChange={async (v) => {
await patchAppConfig({ smartCoreUseLightGBM: v })
await restartCore()
await mihomoHotReloadConfig()
}}
/>
</SettingItem>
@@ -613,7 +613,7 @@ const Mihomo: React.FC = () => {
isSelected={smartCoreCollectData}
onValueChange={async (v) => {
await patchAppConfig({ smartCoreCollectData: v })
await restartCore()
await mihomoHotReloadConfig()
}}
/>
</SettingItem>
@@ -636,7 +636,7 @@ const Mihomo: React.FC = () => {
<div className="flex items-center gap-2">
<Input
size="sm"
className="w-[100px]"
className="w-25"
type="number"
value={smartCollectorSize.toString()}
onValueChange={async (v: string) => {
@@ -650,7 +650,7 @@ const Mihomo: React.FC = () => {
if (isNaN(num)) num = 100
if (num < 1) num = 1
await patchAppConfig({ smartCollectorSize: num })
await restartCore()
await mihomoHotReloadConfig()
}}
/>
<span className="text-default-500">MB</span>
@@ -662,7 +662,7 @@ const Mihomo: React.FC = () => {
classNames={{
trigger: 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50'
}}
className="w-[150px]"
className="w-37.5"
size="sm"
aria-label={t('mihomo.smartCoreStrategy')}
selectedKeys={new Set([smartCoreStrategy])}
@@ -670,7 +670,7 @@ const Mihomo: React.FC = () => {
onSelectionChange={async (v) => {
const strategy = v.currentKey as 'sticky-sessions' | 'round-robin'
await patchAppConfig({ smartCoreStrategy: strategy })
await restartCore()
await mihomoHotReloadConfig()
}}
>
<SelectItem key="sticky-sessions">
@@ -710,7 +710,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={(showMixedPort ?? mixedPort ?? '').toString()}
max={65535}
min={0}
@@ -771,7 +771,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={(showSocksPort ?? socksPort ?? '').toString()}
max={65535}
min={0}
@@ -832,7 +832,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={(showHttpPort ?? httpPort ?? '').toString()}
max={65535}
min={0}
@@ -894,7 +894,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={(showRedirPort ?? redirPort ?? '').toString()}
max={65535}
min={0}
@@ -957,7 +957,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={(showTproxyPort ?? tproxyPort ?? '').toString()}
max={65535}
min={0}
@@ -1029,7 +1029,7 @@ const Mihomo: React.FC = () => {
>
<Input
size="sm"
className={`w-[200px] ${externalControllerError ? 'border-red-500 ring-1 ring-red-500 rounded-lg' : ''}`}
className={`w-50 ${externalControllerError ? 'border-red-500 ring-1 ring-red-500 rounded-lg' : ''}`}
value={externalControllerInput}
onValueChange={(v) => {
setExternalControllerInput(v)
@@ -1081,7 +1081,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type={isSecretVisible ? 'text' : 'password'}
className="w-[200px]"
className="w-50"
value={secretInput}
onValueChange={(v) => {
setSecretInput(v)
@@ -1422,7 +1422,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={maxLogDays.toString()}
onValueChange={(v) => {
const num = parseInt(v)
@@ -1436,7 +1436,7 @@ const Mihomo: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={maxLogFileSize.toString()}
onValueChange={(v) => {
const num = parseInt(v)
@@ -1455,7 +1455,7 @@ const Mihomo: React.FC = () => {
<SettingItem title={t('mihomo.logLevel')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
className="w-25"
size="sm"
aria-label={t('mihomo.selectLogLevel')}
selectedKeys={new Set([logLevel])}
@@ -1474,7 +1474,7 @@ const Mihomo: React.FC = () => {
<SettingItem title={t('mihomo.findProcess')}>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[100px]"
className="w-25"
size="sm"
aria-label={t('mihomo.selectFindProcessMode')}
selectedKeys={new Set([findProcessMode])}
@@ -1506,7 +1506,7 @@ const Mihomo: React.FC = () => {
<ModalBody className="flex flex-col h-full">
<div className="flex flex-col h-full">
{/* 添加/编辑面板表单 */}
<div className="flex flex-col gap-2 p-3 bg-default-100 rounded-lg flex-shrink-0">
<div className="flex flex-col gap-2 p-3 bg-default-100 rounded-lg shrink-0">
<Input
label={t('settings.webui.panelName')}
placeholder={t('settings.webui.panelNamePlaceholder')}
@@ -1563,12 +1563,12 @@ const Mihomo: React.FC = () => {
</div>
{/* 面板列表 */}
<div className="flex flex-col gap-2 mt-2 overflow-y-auto flex-grow">
<div className="flex flex-col gap-2 mt-2 overflow-y-auto grow">
<h3 className="text-lg font-semibold">{t('settings.webui.panels')}</h3>
{allPanels.map((panel) => (
<div
key={panel.id}
className="flex items-start justify-between p-3 bg-default-50 rounded-lg flex-shrink-0"
className="flex items-start justify-between p-3 bg-default-50 rounded-lg shrink-0"
>
<div className="flex-1 mr-2">
<p className="font-medium">{panel.name}</p>

View File

@@ -5,7 +5,7 @@ import SettingCard from '@renderer/components/base/base-setting-card'
import SettingItem from '@renderer/components/base/base-setting-item'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { restartCore, patchMihomoConfig } from '@renderer/utils/ipc'
import { mihomoHotReloadConfig } from '@renderer/utils/ipc'
import React, { ReactNode, useState } from 'react'
import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next'
@@ -67,8 +67,7 @@ const Sniffer: React.FC = () => {
await patchControledMihomoConfig(patch)
if (controlSniff) {
await patchMihomoConfig(patch)
await restartCore()
await mihomoHotReloadConfig()
}
} catch (e) {
showErrorSync(e, t('common.error.snifferConfigSaveFailed'))

View File

@@ -64,7 +64,6 @@ const Tun: React.FC = () => {
const onSave = async (patch: Partial<IMihomoConfig>): Promise<void> => {
await patchControledMihomoConfig(patch)
await restartCore()
setChanged(false)
}
@@ -169,7 +168,7 @@ const Tun: React.FC = () => {
<SettingItem title={t('tun.device.title')} divider>
<Input
size="sm"
className="w-[100px]"
className="w-25"
value={values.device}
placeholder={platform === 'darwin' ? 'utun1500' : 'Mihomo'}
onValueChange={(v) => {
@@ -220,7 +219,7 @@ const Tun: React.FC = () => {
<Input
size="sm"
type="number"
className="w-[100px]"
className="w-25"
value={values.mtu.toString()}
onValueChange={(v) => {
const num = parseInt(v)

View File

@@ -83,6 +83,7 @@ interface IpcApi {
openFile: (type: 'profile' | 'override', id: string, ext?: 'yaml' | 'js') => Promise<void>
// Core
restartCore: () => Promise<void>
mihomoHotReloadConfig: () => Promise<void>
startMonitor: () => Promise<void>
quitWithoutCore: () => Promise<void>
// System
@@ -240,6 +241,7 @@ export const {
openFile,
// Core
restartCore,
mihomoHotReloadConfig,
startMonitor,
quitWithoutCore,
// System
@@ -361,7 +363,7 @@ export async function getAppName(appPath: string): Promise<string> {
return invoke<string>('getAppName', appPath)
}
// getIconDataURL: 获取应用图标的Base64数据
// getIconDataURL: 获取应用图标的 Base64 数据
export async function getIconDataURL(appPath: string): Promise<string> {
return invoke<string>('getIconDataURL', appPath)
}

View File

@@ -358,6 +358,7 @@ interface IAppConfig {
showTproxyPort?: number
enableTproxyPort?: boolean
testProfileOnStart?: boolean
useHotReloadProfile?: boolean
}
interface IMihomoTunConfig {