Compare commits

...

11 Commits

28 changed files with 3197 additions and 2713 deletions

View File

@ -63,7 +63,7 @@ jobs:
# Delete each asset with detailed logging
echo "$ALL_ASSETS" | jq -r '.[].id' | while read asset_id; do
if [ ! -z "$asset_id" ]; then
echo "🗑️ Deleting asset ID: $asset_id"
echo "🗑️ Deleting asset ID: $asset_id"
RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id")
@ -102,9 +102,14 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
env:
npm_config_arch: ${{ matrix.arch }}
@ -136,7 +141,7 @@ jobs:
run: pnpm copy-legacy
- name: Upload Artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Windows ${{ matrix.arch }}
path: |
@ -178,9 +183,14 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
env:
npm_config_arch: ${{ matrix.arch }}
@ -214,7 +224,7 @@ jobs:
run: pnpm copy-legacy
- name: Upload Artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Win7 ${{ matrix.arch }}
path: |
@ -256,9 +266,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
env:
npm_config_arch: ${{ matrix.arch }}
@ -285,7 +300,7 @@ jobs:
run: pnpm copy-legacy
- name: Upload Artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Linux ${{ matrix.arch }}
path: |
@ -327,9 +342,14 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
env:
npm_config_arch: ${{ matrix.arch }}
@ -357,7 +377,7 @@ jobs:
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
pnpm build:mac --${{ matrix.arch }}
- name: Setup temporary installer signing keychain
uses: apple-actions/import-codesign-certs@v3
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
@ -381,7 +401,7 @@ jobs:
run: pnpm copy-legacy
- name: Upload Artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: MacOS ${{ matrix.arch }}
path: |
@ -420,9 +440,14 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
env:
npm_config_arch: ${{ matrix.arch }}
@ -452,7 +477,7 @@ jobs:
chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
pnpm build:mac --${{ matrix.arch }}
- name: Setup temporary installer signing keychain
uses: apple-actions/import-codesign-certs@v3
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ secrets.CSC_INSTALLER_LINK }}
p12-password: ${{ secrets.CSC_INSTALLER_KEY_PASSWORD }}
@ -476,7 +501,7 @@ jobs:
run: pnpm copy-legacy
- name: Upload Artifacts
if: startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Catalina ${{ matrix.arch }}
path: |
@ -509,9 +534,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup pnpm
run: npm install -g pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install
- name: Update Version for Dev Build
@ -566,7 +596,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Update Version
run: |
sed -i "s/pkgver=.*/pkgver=$(echo ${{ github.ref }} | tr -d 'refs/tags/v')/" aur/${{ matrix.pkgname }}/PKGBUILD
@ -601,7 +631,7 @@ jobs:
if: startsWith(github.ref, 'refs/heads/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: update version

3
.npmrc
View File

@ -1,3 +1,4 @@
shamefully-hoist=true
virtual-store-dir-max-length=80
public-hoist-pattern[]=*@heroui/*
public-hoist-pattern[]=*@heroui/*
only-built-dependencies="['electron', 'esbuild', 'meta-json-schema']"

View File

@ -72,5 +72,8 @@ deb:
rpm:
afterInstall: 'build/linux/postinst'
afterRemove: 'build/linux/postuninst'
fpm:
- '--rpm-rpmbuild-define'
- '_build_id_links none'
npmRebuild: true
publish: []

View File

@ -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'
}

View File

@ -10,7 +10,7 @@
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"prepare": "node scripts/prepare.mjs",
"prepare:dev": "node scripts/update-version.mjs && node scripts/prepare.mjs",
"updater": "node scripts/updater.mjs",
@ -23,33 +23,26 @@
"dev": "electron-vite dev",
"postinstall": "electron-builder install-app-deps && node node_modules/electron/install.js",
"build:win": "electron-vite build && electron-builder --publish never --win",
"build:win:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --win",
"build:win:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --win",
"build:mac": "electron-vite build && electron-builder --publish never --mac",
"build:mac:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --mac",
"build:mac:dev": "pnpm run prepare:dev && electron-vite build && electron-builder --publish never --mac",
"build:linux": "electron-vite build && electron-builder --publish never --linux",
"build:linux:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
"build:linux:dev": "pnpm 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",
"chokidar": "^5.0.0",
"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",
"express": "^5.2.1",
"i18next": "^25.7.3",
"iconv-lite": "^0.7.1",
"webdav": "^5.8.0",
"ws": "^8.18.3",
"yaml": "^2.8.1"
"yaml": "^2.8.2"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
@ -57,49 +50,55 @@
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@tailwindcss/vite": "^4.1.17",
"@electron-toolkit/tsconfig": "^2.0.0",
"@heroui/react": "^2.8.7",
"@tailwindcss/vite": "^4.1.18",
"@types/adm-zip": "^0.5.7",
"@types/express": "^5.0.5",
"@types/node": "^24.10.1",
"@types/crypto-js": "^4.2.2",
"@types/express": "^5.0.6",
"@types/node": "^25.0.3",
"@types/pubsub-js": "^1.8.6",
"@types/react": "^19.2.5",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.7.0",
"@vitejs/plugin-react": "^5.1.2",
"chart.js": "^4.5.1",
"cron-validator": "^1.4.0",
"driver.js": "^1.3.6",
"electron": "^37.10.0",
"dayjs": "^1.11.19",
"driver.js": "^1.4.0",
"electron": "^39.2.7",
"electron-builder": "26.0.12",
"electron-vite": "^4.0.1",
"electron-vite": "^5.0.0",
"electron-window-state": "^5.0.3",
"eslint": "9.32.0",
"eslint": "9.39.2",
"eslint-plugin-react": "^7.37.5",
"form-data": "^4.0.4",
"framer-motion": "12.23.12",
"form-data": "^4.0.5",
"framer-motion": "12.23.26",
"lodash": "^4.17.21",
"meta-json-schema": "^1.19.16",
"meta-json-schema": "^1.19.17",
"monaco-yaml": "^5.4.0",
"nanoid": "^5.1.6",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"pubsub-js": "^1.9.5",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.3",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.3",
"react-error-boundary": "^6.0.0",
"react-i18next": "^16.5.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-monaco-editor": "^0.59.0",
"react-router-dom": "^7.9.6",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tailwindcss": "^4.1.17",
"react-router-dom": "^7.11.0",
"react-virtuoso": "^4.18.0",
"swr": "^2.3.8",
"tailwindcss": "^4.1.18",
"tar": "^7.5.2",
"tsx": "^4.20.6",
"tsx": "^4.21.0",
"types-pac": "^1.0.3",
"typescript": "^5.9.3",
"vite": "^7.2.2",
"vite": "^7.3.0",
"vite-plugin-monaco-editor": "^1.1.0"
},
"packageManager": "pnpm@10.22.0"

5100
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import { deepMerge } from '../utils/merge'
import { defaultConfig } from '../utils/template'
let appConfig: IAppConfig // config.yaml
let appConfigWriteQueue: Promise<void> = Promise.resolve()
export async function getAppConfig(force = false): Promise<IAppConfig> {
if (force || !appConfig) {
@ -22,9 +23,12 @@ export async function getAppConfig(force = false): Promise<IAppConfig> {
}
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
if (patch.nameserverPolicy) {
appConfig.nameserverPolicy = patch.nameserverPolicy
}
appConfig = deepMerge(appConfig, patch)
await writeFile(appConfigPath(), stringify(appConfig))
appConfigWriteQueue = appConfigWriteQueue.then(async () => {
if (patch.nameserverPolicy) {
appConfig.nameserverPolicy = patch.nameserverPolicy
}
appConfig = deepMerge(appConfig, patch)
await writeFile(appConfigPath(), stringify(appConfig))
})
await appConfigWriteQueue
}

View File

@ -6,6 +6,7 @@ import * as chromeRequest from '../utils/chromeRequest'
import { parse, stringify } from '../utils/yaml'
let overrideConfig: IOverrideConfig // override.yaml
let overrideConfigWriteQueue: Promise<void> = Promise.resolve()
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
if (force || !overrideConfig) {
@ -17,8 +18,11 @@ export async function getOverrideConfig(force = false): Promise<IOverrideConfig>
}
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
overrideConfig = config
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
overrideConfigWriteQueue = overrideConfigWriteQueue.then(async () => {
overrideConfig = config
await writeFile(overrideConfigPath(), stringify(overrideConfig), 'utf-8')
})
await overrideConfigWriteQueue
}
export async function getOverrideItem(id: string | undefined): Promise<IOverrideItem | undefined> {

View File

@ -15,8 +15,8 @@ import { mihomoUpgradeConfig } from '../core/mihomoApi'
import i18next from 'i18next'
let profileConfig: IProfileConfig // profile.yaml
// 最终选中订阅ID
let profileConfig: IProfileConfig
let profileConfigWriteQueue: Promise<void> = Promise.resolve()
let targetProfileId: string | null = null
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
@ -25,12 +25,31 @@ export async function getProfileConfig(force = false): Promise<IProfileConfig> {
profileConfig = parse(data) || { items: [] }
}
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
return profileConfig
return structuredClone(profileConfig)
}
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
profileConfig = config
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
profileConfig = config
await writeFile(profileConfigPath(), stringify(config), 'utf-8')
})
await profileConfigWriteQueue
}
export async function updateProfileConfig(
updater: (config: IProfileConfig) => IProfileConfig | Promise<IProfileConfig>
): Promise<IProfileConfig> {
let result: IProfileConfig
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
const data = await readFile(profileConfigPath(), 'utf-8')
profileConfig = parse(data) || { items: [] }
if (typeof profileConfig !== 'object') profileConfig = { items: [] }
profileConfig = await updater(structuredClone(profileConfig))
result = profileConfig
await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8')
})
await profileConfigWriteQueue
return structuredClone(result!)
}
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
@ -41,8 +60,7 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
}
export async function changeCurrentProfile(id: string): Promise<void> {
const config = await getProfileConfig()
const current = config.current
const { current } = await getProfileConfig()
if (current === id && targetProfileId !== id) {
return
@ -50,13 +68,12 @@ export async function changeCurrentProfile(id: string): Promise<void> {
targetProfileId = id
config.current = id
const configSavePromise = setProfileConfig(config)
try {
await configSavePromise
await updateProfileConfig((config) => {
config.current = id
return config
})
// 检查订阅切换是否中断
if (targetProfileId !== id) {
return
}
@ -66,8 +83,10 @@ export async function changeCurrentProfile(id: string): Promise<void> {
}
} catch (e) {
if (targetProfileId === id) {
config.current = current
await setProfileConfig(config)
await updateProfileConfig((config) => {
config.current = current
return config
})
targetProfileId = null
throw e
}
@ -75,47 +94,51 @@ export async function changeCurrentProfile(id: string): Promise<void> {
}
export async function updateProfileItem(item: IProfileItem): Promise<void> {
const config = await getProfileConfig()
const index = config.items.findIndex((i) => i.id === item.id)
if (index === -1) {
throw new Error('Profile not found')
}
config.items[index] = item
await setProfileConfig(config)
await updateProfileConfig((config) => {
const index = config.items.findIndex((i) => i.id === item.id)
if (index === -1) {
throw new Error('Profile not found')
}
config.items[index] = item
return config
})
}
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item)
const config = await getProfileConfig()
if (await getProfileItem(newItem.id)) {
await updateProfileItem(newItem)
} else {
config.items.push(newItem)
}
await setProfileConfig(config)
let shouldChangeCurrent = false
await updateProfileConfig((config) => {
const existingIndex = config.items.findIndex((i) => i.id === newItem.id)
if (existingIndex !== -1) {
config.items[existingIndex] = newItem
} else {
config.items.push(newItem)
}
if (!config.current) {
shouldChangeCurrent = true
}
return config
})
if (!config.current) {
if (shouldChangeCurrent) {
await changeCurrentProfile(newItem.id)
}
await addProfileUpdater(newItem)
}
export async function removeProfileItem(id: string): Promise<void> {
// 先清理自动更新定时器,防止已删除的订阅重新出现
await removeProfileUpdater(id)
const config = await getProfileConfig()
config.items = config.items?.filter((item) => item.id !== id)
let shouldRestart = false
if (config.current === id) {
shouldRestart = true
if (config.items.length > 0) {
config.current = config.items[0].id
} else {
config.current = undefined
await updateProfileConfig((config) => {
config.items = config.items?.filter((item) => item.id !== id)
if (config.current === id) {
shouldRestart = true
config.current = config.items.length > 0 ? config.items[0].id : undefined
}
}
await setProfileConfig(config)
return config
})
if (existsSync(profilePath(id))) {
await rm(profilePath(id))
}

View File

@ -56,7 +56,7 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
return { normalRules, insertRules: rules }
}
export async function generateProfile(): Promise<void> {
export async function generateProfile(): Promise<string | undefined> {
// 读取最新的配置
const { current } = await getProfileConfig(true)
const {
@ -150,6 +150,7 @@ export async function generateProfile(): Promise<void> {
diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'),
runtimeConfigStr
)
return current
}
async function prepareProfileWorkDir(current: string | undefined): Promise<void> {

View File

@ -13,7 +13,6 @@ import { generateProfile } from './factory'
import {
getAppConfig,
getControledMihomoConfig,
getProfileConfig,
patchAppConfig,
patchControledMihomoConfig,
manageSmartOverride
@ -130,15 +129,15 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
await rm(path.join(dataDir(), 'core.pid'))
}
}
const { current } = await getProfileConfig(true)
const { tun } = await getControledMihomoConfig()
const corePath = mihomoCorePath(core)
// 管理 Smart 内核覆写配置
await manageSmartOverride()
await generateProfile()
await checkProfile()
// generateProfile 返回实际使用的 current确保内核工作目录与配置文件一致
const current = await generateProfile()
await checkProfile(current)
await stopCore()
await cleanupSocketFile()
@ -453,9 +452,8 @@ export async function quitWithoutCore(): Promise<void> {
app.exit()
}
async function checkProfile(): Promise<void> {
async function checkProfile(current: string | undefined): Promise<void> {
const { core = 'mihomo', diffWorkDir = false } = await getAppConfig()
const { current } = await getProfileConfig()
const corePath = mihomoCorePath(core)
const execFilePromise = promisify(execFile)
@ -562,7 +560,7 @@ async function waitForCoreReady(): Promise<void> {
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 +789,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
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 +833,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
}
}
}
} catch (error) {
} catch {
// ignore
}
}

View File

@ -19,6 +19,8 @@ let logsRetry = 10
let mihomoConnectionsWs: WebSocket | null = null
let connectionsRetry = 10
const MAX_RETRY = 10
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
const dynamicIpcPath = getMihomoIpcPath()
@ -233,6 +235,7 @@ export const mihomoSmartFlushCache = async (configName?: string): Promise<void>
}
export const startMihomoTraffic = async (): Promise<void> => {
trafficRetry = MAX_RETRY
await mihomoTraffic()
}
@ -258,7 +261,7 @@ const mihomoTraffic = async (): Promise<void> => {
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
const data = e.data as string
const json = JSON.parse(data) as IMihomoTrafficInfo
trafficRetry = 10
trafficRetry = MAX_RETRY
try {
mainWindow?.webContents.send('mihomoTraffic', json)
if (process.platform !== 'linux') {
@ -292,6 +295,7 @@ const mihomoTraffic = async (): Promise<void> => {
}
export const startMihomoMemory = async (): Promise<void> => {
memoryRetry = MAX_RETRY
await mihomoMemory()
}
@ -314,7 +318,7 @@ const mihomoMemory = async (): Promise<void> => {
mihomoMemoryWs.onmessage = (e): void => {
const data = e.data as string
memoryRetry = 10
memoryRetry = MAX_RETRY
try {
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
} catch {
@ -338,6 +342,7 @@ const mihomoMemory = async (): Promise<void> => {
}
export const startMihomoLogs = async (): Promise<void> => {
logsRetry = MAX_RETRY
await mihomoLogs()
}
@ -362,7 +367,7 @@ const mihomoLogs = async (): Promise<void> => {
mihomoLogsWs.onmessage = (e): void => {
const data = e.data as string
logsRetry = 10
logsRetry = MAX_RETRY
try {
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
} catch {
@ -386,6 +391,7 @@ const mihomoLogs = async (): Promise<void> => {
}
export const startMihomoConnections = async (): Promise<void> => {
connectionsRetry = MAX_RETRY
await mihomoConnections()
}
@ -408,7 +414,7 @@ const mihomoConnections = async (): Promise<void> => {
mihomoConnectionsWs.onmessage = (e): void => {
const data = e.data as string
connectionsRetry = 10
connectionsRetry = MAX_RETRY
try {
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
} catch {

View File

@ -1,33 +1,40 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config'
import { Cron } from 'croner'
import { logger } from '../utils/logger'
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
async function updateProfile(id: string): Promise<void> {
const item = await getProfileItem(id)
if (item && item.type === 'remote') {
await addProfileItem(item)
}
}
export async function initProfileUpdater(): Promise<void> {
const { items, current } = await getProfileConfig()
const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.autoUpdate && item.interval) {
const itemId = item.id
if (typeof item.interval === 'number') {
// 数字间隔使用 setInterval
intervalPool[item.id] = setInterval(
intervalPool[itemId] = setInterval(
async () => {
try {
await addProfileItem(item)
await updateProfile(itemId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
}
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
// 字符串间隔使用 Cron
intervalPool[item.id] = new Cron(item.interval, async () => {
intervalPool[itemId] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
await updateProfile(itemId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
}
})
}
@ -35,19 +42,20 @@ export async function initProfileUpdater(): Promise<void> {
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to init profile ${item.name}:`, e)
}
}
}
if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) {
const currentId = currentItem.id
if (typeof currentItem.interval === 'number') {
intervalPool[currentItem.id] = setInterval(
intervalPool[currentId] = setInterval(
async () => {
try {
await addProfileItem(currentItem)
await updateProfile(currentId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
}
},
currentItem.interval * 60 * 1000
@ -56,19 +64,19 @@ export async function initProfileUpdater(): Promise<void> {
setTimeout(
async () => {
try {
await addProfileItem(currentItem)
await updateProfile(currentId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
}
},
currentItem.interval * 60 * 1000 + 10000 // +10s
currentItem.interval * 60 * 1000 + 10000
)
} else if (typeof currentItem.interval === 'string') {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
intervalPool[currentId] = new Cron(currentItem.interval, async () => {
try {
await addProfileItem(currentItem)
await updateProfile(currentId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
}
})
}
@ -76,7 +84,7 @@ export async function initProfileUpdater(): Promise<void> {
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to init current profile:`, e)
}
}
}
@ -91,23 +99,24 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
}
}
const itemId = item.id
if (typeof item.interval === 'number') {
intervalPool[item.id] = setInterval(
intervalPool[itemId] = setInterval(
async () => {
try {
await addProfileItem(item)
await updateProfile(itemId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
}
},
item.interval * 60 * 1000
)
} else if (typeof item.interval === 'string') {
intervalPool[item.id] = new Cron(item.interval, async () => {
intervalPool[itemId] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
await updateProfile(itemId)
} catch (e) {
/* ignore */
await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
}
})
}

View File

@ -120,7 +120,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
groupsMenu = groupItems
groupsMenu.unshift({ type: 'separator' })
}
} catch (e) {
} catch {
// ignore
// 避免出错时无法创建托盘菜单
}
@ -206,7 +206,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
await patchAppConfig({ sysProxy: { enable } })
mainWindow?.webContents.send('appConfigUpdated')
floatingWindow?.webContents.send('appConfigUpdated')
} catch (e) {
} catch {
// ignore
} finally {
ipcMain.emit('updateTrayMenu')
@ -390,6 +390,8 @@ export async function createTray(): Promise<void> {
if (!useDockIcon) {
hideDockIcon()
}
// 移除旧监听器防止累积
ipcMain.removeAllListeners('trayIconUpdate')
ipcMain.on('trayIconUpdate', async (_, png: string) => {
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
image.setTemplateImage(true)
@ -435,6 +437,8 @@ export async function createTray(): Promise<void> {
triggerMainWindow()
}
})
// 移除旧监听器防止累积
ipcMain.removeAllListeners('updateTrayMenu')
ipcMain.on('updateTrayMenu', async () => {
await updateTrayMenu()
})

View File

@ -61,7 +61,7 @@ export async function checkAutoRun(): Promise<boolean> {
`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<void> {
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<void> {
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?')
}
}

View File

@ -160,18 +160,33 @@ function isSocketFileExists(): boolean {
}
}
// Helper function to send signal to recreate socket
// Check if helper process is running (no admin privileges needed)
async function isHelperRunning(): Promise<boolean> {
try {
const execPromise = promisify(exec)
const { stdout } = await execPromise('pgrep -f party.mihomo.helper')
return stdout.trim().length > 0
} catch {
return false
}
}
// Start or restart helper service via launchctl
async function startHelperService(): Promise<void> {
const execPromise = promisify(exec)
const shell = `launchctl kickstart -k system/party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`)
await new Promise((resolve) => setTimeout(resolve, 1500))
}
// Send signal to recreate socket (only if process is running)
async function requestSocketRecreation(): Promise<void> {
try {
// Send SIGUSR1 signal to helper process to recreate socket
const execPromise = promisify(exec)
// Use osascript with administrator privileges (same pattern as grantTunPermissions)
const shell = `pkill -USR1 -f party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`)
// Wait a bit for socket recreation
await new Promise((resolve) => setTimeout(resolve, 1000))
} catch (error) {
await proxyLogger.error('Failed to send signal to helper', error)
@ -180,7 +195,7 @@ async function requestSocketRecreation(): Promise<void> {
}
// Wrapper function for helper requests with auto-retry on socket issues
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 2): Promise<unknown> {
let lastError: Error | null = null
for (let attempt = 0; attempt <= maxRetries; attempt++) {
@ -188,21 +203,34 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
return await requestFn()
} catch (error) {
lastError = error as Error
const errCode = (error as NodeJS.ErrnoException).code
const errMsg = (error as Error).message || ''
// Check if it's a connection error and socket file doesn't exist
if (
attempt < maxRetries &&
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
(error as Error).message?.includes('connect ECONNREFUSED') ||
(error as Error).message?.includes('ENOENT'))
(errCode === 'ECONNREFUSED' ||
errCode === 'ENOENT' ||
errMsg.includes('connect ECONNREFUSED') ||
errMsg.includes('ENOENT'))
) {
await proxyLogger.info(
`Helper request failed (attempt ${attempt + 1}), checking socket file...`
`Helper request failed (attempt ${attempt + 1}/${maxRetries + 1}), checking helper status...`
)
if (!isSocketFileExists()) {
await proxyLogger.info('Socket file missing, requesting recreation...')
const helperRunning = await isHelperRunning()
const socketExists = isSocketFileExists()
if (!helperRunning) {
await proxyLogger.info('Helper process not running, starting service...')
try {
await startHelperService()
await proxyLogger.info('Helper service started, retrying...')
continue
} catch (startError) {
await proxyLogger.warn('Failed to start helper service', startError)
}
} else if (!socketExists) {
await proxyLogger.info('Socket file missing but helper running, requesting recreation...')
try {
await requestSocketRecreation()
await proxyLogger.info('Socket recreation requested, retrying...')
@ -213,7 +241,6 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
}
}
// If not a connection error or we've exhausted retries, throw the error
if (attempt === maxRetries) {
throw lastError
}

View File

@ -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<unknown>
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

View File

@ -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<ListenChannel, Set<IpcListener>>()
// 安全的 IPC API只暴露白名单内的 channels
const electronAPI = {
ipcRenderer: {
invoke: (channel: InvokeChannel, ...args: unknown[]): Promise<unknown> => {
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)

View File

@ -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
}
}

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState, useCallback } from 'react'
import MihomoIcon from './components/base/mihomo-icon'
import { calcTraffic } from './utils/calc'
import { showContextMenu, triggerMainWindow } from './utils/ipc'
@ -48,16 +48,19 @@ const FloatingApp: React.FC = () => {
}
}, [spinSpeed, spinFloatingIcon])
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
setUpload(info.up)
setDownload(info.down)
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
}
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
const info = args[0] as IMihomoTrafficInfo
setUpload(info.up)
setDownload(info.down)
}, [])
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
return (): void => {
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
}
}, [handleTraffic])
return (
<div className="app-drag h-screen w-screen overflow-hidden">
<div className="floating-bg border border-divider flex bg-content1 h-full w-full">

View File

@ -32,7 +32,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
// @ts-ignore windowControlsOverlay
const windowControlsOverlay = window.navigator.windowControlsOverlay
setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
} catch (e) {
} catch {
// ignore
}
}

View File

@ -149,7 +149,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
// 非纯数字
try {
setValues({ ...values, interval: v })
} catch (e) {
} catch {
// ignore
}
}

View File

@ -66,7 +66,7 @@ const Viewer: React.FC<Props> = (props) => {
})
)
}
} catch (error) {
} catch {
setCurrData(fileContent)
}
} finally {

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import React, { useEffect, useState, useMemo } from 'react'
import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5'
@ -25,11 +25,6 @@ import { useTranslation } from 'react-i18next'
// 注册 Chart.js 组件
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Filler)
let currentUpload: number | undefined = undefined
let currentDownload: number | undefined = undefined
let hasShowTraffic = false
let drawing = false
interface Props {
iconOnly?: boolean
}
@ -61,6 +56,12 @@ const ConnCard: React.FC<Props> = (props) => {
})
const [series, setSeries] = useState(Array(10).fill(0))
// 使用 useRef 替代模块级变量
const currentUploadRef = useRef<number | undefined>(undefined)
const currentDownloadRef = useRef<number | undefined>(undefined)
const hasShowTrafficRef = useRef(false)
const drawingRef = useRef(false)
// Chart.js 配置
const chartData = useMemo(() => {
return {
@ -125,35 +126,45 @@ const ConnCard: React.FC<Props> = (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) => {
// 使用 useCallback 创建稳定的 handler 引用
const handleTraffic = useCallback(
async (_e: unknown, ...args: unknown[]) => {
const info = args[0] as IMihomoTrafficInfo
setUpload(info.up)
setDownload(info.down)
const data = series
data.shift()
data.push(info.up + info.down)
setSeries([...data])
setSeries((prev) => {
const data = [...prev]
data.shift()
data.push(info.up + info.down)
return data
})
if (platform === 'darwin' && showTraffic) {
if (drawing) return
drawing = true
if (drawingRef.current) return
drawingRef.current = true
try {
await drawSvg(info.up, info.down)
hasShowTraffic = true
await drawSvg(info.up, info.down, currentUploadRef, currentDownloadRef)
hasShowTrafficRef.current = true
} catch {
// ignore
} finally {
drawing = false
drawingRef.current = false
}
} else {
if (!hasShowTraffic) return
if (!hasShowTrafficRef.current) return
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64)
hasShowTraffic = false
hasShowTrafficRef.current = false
}
})
},
[showTraffic]
)
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
window.electron.ipcRenderer.removeListener('mihomoTraffic', handleTraffic)
}
}, [showTraffic])
}, [handleTraffic])
if (iconOnly) {
return (
@ -273,10 +284,15 @@ const ConnCard: React.FC<Props> = (props) => {
export default ConnCard
const drawSvg = async (upload: number, download: number): Promise<void> => {
if (upload === currentUpload && download === currentDownload) return
currentUpload = upload
currentDownload = download
const drawSvg = async (
upload: number,
download: number,
currentUploadRef: React.RefObject<number | undefined>,
currentDownloadRef: React.RefObject<number | undefined>
): Promise<void> => {
if (upload === currentUploadRef.current && download === currentDownloadRef.current) return
currentUploadRef.current = upload
currentDownloadRef.current = download
const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="140" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(upload)}/s</text><text x="140" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end">${calcTraffic(download)}/s</text></svg>`
const image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image)

View File

@ -43,7 +43,8 @@ const MihomoCoreCard: React.FC<Props> = (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 => {

View File

@ -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

View File

@ -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) {

View File

@ -1,8 +1,8 @@
import { TitleBarOverlayOptions } from 'electron'
function checkIpcError<T>(response: T | { invokeError: unknown }): T {
function checkIpcError<T>(response: unknown): T {
if (response && typeof response === 'object' && 'invokeError' in response) {
throw response.invokeError
throw (response as { invokeError: unknown }).invokeError
}
return response as T
}